MCP 2026-07-28: deprecation notices and documentation - #234
Open
simonx1 wants to merge 26 commits into
Open
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 1, 2026 23:18
c4bc3de to
8302d6c
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
2 times, most recently
from
September 1, 2026 23:30
ff00d1c to
7c035c2
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 1, 2026 23:52
c66edee to
868c566
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 1, 2026 23:55
610e4d5 to
0194107
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 00:24
647d7f3 to
2592218
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 00:25
93a3410 to
7e0ed7e
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
2 times, most recently
from
September 2, 2026 05:09
da6872c to
47c020c
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 05:11
7e0ed7e to
1788a52
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 07:28
7ff45a0 to
e5f54ed
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 07:31
1788a52 to
452193d
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 17:02
f6de20d to
1f3343d
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 17:07
5c5663e to
2678302
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 18:10
1f3343d to
c0e5832
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 18:12
3f5ee6f to
48481b0
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 19:10
c0e5832 to
5c2ef8d
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 19:14
48481b0 to
8414792
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 19:56
5c2ef8d to
d24d10f
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 19:59
8414792 to
9d3e767
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 20:38
d24d10f to
20afd3d
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 20:40
9d3e767 to
acc48a7
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 21:22
20afd3d to
095de23
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 21:25
acc48a7 to
36d5582
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 22:04
095de23 to
0e85781
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 22:06
36d5582 to
40e2988
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 23:07
0e85781 to
9a02391
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 2, 2026 23:09
40e2988 to
a81ba60
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 2, 2026 23:56
9a02391 to
6e1c2e5
Compare
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 6, 2026 15:42
9127c10 to
9893fa4
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 6, 2026 15:42
9178a10 to
fa89ee4
Compare
…support MCP 2026-07-28 places Roots, Sampling, Logging, the HTTP+SSE transport, the includeContext values thisServer/allServers and Dynamic Client Registration in the Deprecated state of its feature lifecycle policy (SEP-2577, SEP-2596, PR #2858). They keep working; MCPClient::Deprecations now names them (REGISTRY) and logs one notice per feature per process on first use with the suggested migration, silenced with Deprecations.enabled = false. README documents the 2026-07-28 features (discovery and per-request metadata, multi round-trip requests, x-mcp-header, subscriptions, cacheable results, the tasks extension, authorization) and the deprecations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…stry - client.rb, server_sse.rb and oauth_provider.rb require the deprecations module themselves, so `require 'mcp_client/client'` keeps working with the deprecated options. - The Dynamic Client Registration notice goes through MCPClient::Deprecations too: once per process, silenced with the rest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…nnect, logging entry points - The registry records when each feature entered the Deprecated state (HTTP+SSE since 2025-03-26, includeContext values since 2025-11-25) and the docs no longer promise twelve months for those two. - The HTTP+SSE notice is logged once the transport is connected, so MCPClient.connect probing a URL that ends up elsewhere does not spend it. - Logging warns on every entry point: log_level= on a client or a server and an incoming notifications/message. - A logger that drops warnings or raises does not consume the notice; the Unicode line separators are escaped in quoted detail. - Deprecated APIs carry YARD @deprecated tags; README names the prefixed _meta keys and where protocol:/discover_timeout: live; OAUTH.md and the README OAuth summary treat Dynamic Client Registration as the deprecated fallback and show the iss parameter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…ock-free logging - A logger that raises is swallowed by Deprecations.warn (the slot is released for a later use) so the deprecated feature keeps working: an established HTTP+SSE connection is not torn down, includeContext requests are still served, log_level= still runs. - The slot is reserved under the lock and the logger is called outside it, so a logger that consults the registry cannot deadlock and slow log I/O does not stall other threads. - Only a real Logger with a level above WARN skips the notice (no warn? call on strict doubles). - Each transport's log_level= carries a YARD @deprecated tag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
A Logger subclass whose `level` accessor raises aborted the deprecated operation itself, because the level probe ran before the protected path. Ask through `accepts_warnings?`, which rescues and declines the notice without spending its once-per-process slot, so no logger failure ever reaches the feature it guards. Record each feature's `earliest_removal` in the registry and stop the module comment and the README from lumping the includeContext values in with the HTTP+SSE transport: only the transport has the short clock (three months after SEP-2596 is Final), while SEP-2596's transition provision ties includeContext to Sampling's twelve-month window. The README table now carries the registry's strings verbatim and a spec pins them, so the prose cannot drift from the registry again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…ces, fork safety
The earliest removal of each deprecated feature now carries the
2026-07-28 deprecated-features registry's own "Earliest removal" wording
instead of a paraphrase of the lifecycle policy floor: Roots, Sampling,
Logging and Dynamic Client Registration wait for "the first revision
released on or after 2027-07-28" (a revision, not a date a host can plan
around), the includeContext values "follow Sampling (SEP-2577)", and only
the HTTP+SSE transport has a clock of its own ("three months after
SEP-2596 reaches Final"). The runtime notice interpolates it, and every
YARD @deprecated tag names it beside the deprecation SEP, which is what
the feature lifecycle policy's tier-1 SDK obligation asks of an SDK.
The notices also had to move off MCPClient::Client. on_roots_list_request,
on_sampling_request and on_notification are public transport APIs: a host
can negotiate and serve Roots, Sampling and Logging without ever
constructing a Client, and the constructor- and Client-only notices never
fired for it. A new MCPClient::DeprecationNotices mixin, included in
JsonRpcCommon so every transport has it, warns where the callback dispatch
actually invokes the handler -- in each transport's handle_roots_list and
handle_sampling_create_message (with the includeContext values that ride
along on the request), in the multi round-trip input-request path that
reaches the same callbacks, and in SubscriptionSupport#route_notification
for notifications/message, so a bare on_notification callback is covered
too.
Two more holes in the once-only bookkeeping. ServerSSE#connect left the
notice unspent when the first connection found notices disabled or a
logger that drops or raises on warnings, but returned at the
already-connected fast path afterwards, so a long-lived connection never
retried it; that branch now gets its own chance. And Deprecations tracks
the PID that owns @emitted, so a prefork deployment (Puma, Unicorn) that
warned while preloading no longer hands every worker a spent set and
silences the worker's own first use.
The round 5 README pin iterated the registry against the whole section, so
one shared string satisfied four features; it now resolves each feature's
own table row first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…I windows Round 6 moved the Roots notice into the transports, where it fired for any host serving a roots/list request. MCPClient::Client registers a roots/list handler on every server unconditionally (so a later `roots=` is served without reconnecting) and answers with an empty list until a root is set, so every default Client that answered a roots/list request was told it was using a deprecated feature it had never opted into. The Roots notice now follows use of the feature, not the presence of a handler: `warn_roots_deprecated` takes the answer about to be served and warns only when it carries a root. The three transports that serve server-initiated roots/list requests raise it after the handler returns, and the multi round-trip path splits its notice in two — Sampling before the handler (asking is the use), Roots after it. A configured `roots:`, a `Client#roots=` call and a non-empty answer all warn; the default empty answer warns nothing. README states the trigger it actually has. The deprecated keyword arguments now name the earliest removal next to their SEP, as every `@deprecated` tag already did: `Client.new`'s `roots:` and `sampling_handler:`, and `MCPClient.connect`'s `sampling_handler` option, which carried no deprecation at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
`roots=` tells every connected server the list changed, so the example built around a `true` command spent the full fifteen-second discovery timeout waiting for a process that never answers — fifteen seconds added to every full run. Stubbing the notification keeps what the example asserts (the notice fires on the call itself) and takes the file from 15.01s to 0.02s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…ck tags SEP-2596 puts the HTTP+SSE transport on the only short clock in the deprecated features registry — three months after SEP-2596 reaches Final — and it is the transport a new integration is most likely to reach for first. Mark it wherever it is presented as an ordinary choice, consistently with the deprecated-features table and the ServerSSE class tag: the README Overview bullet, the Quick Connect example (which now leads with Streamable HTTP) and the transport-detection table, and in YARD the /sse target bullet and the transport: option of MCPClient.connect, its SSE @example, and MCPClient.sse_config, which gains a @deprecated tag pointing at MCPClient.streamable_http_config. Tag on_roots_list_request and on_sampling_request on ServerStdio, ServerSSE, ServerHTTP and ServerStreamableHTTP @deprecated with SEP-2577 and the earliest removal, so a host that drives a transport directly meets the deprecation in the YARD rather than in the first runtime notice. Round 7 made a registered roots/list handler deliberately silent at runtime, so the Roots tag is the only mark that handler gets: it says registering is not itself use and that an answer carrying a root is. The round 7 doc-block sweep is extended to the eight transport callbacks (same SEP-and-window assertions), and the round 6 tag sweep's floor is raised to the nine new @deprecated tags. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
A deprecation notice was marked spent the moment a caller claimed the right to log it, so two first uses racing with different loggers could lose it outright: the claiming caller blocked inside a broken logger, the contender holding a working one saw the slot taken and stood down, and the claim's later failure left the notice owed to a use that might never come. A feature is now :pending while a caller is inside logger.warn and :emitted only once one succeeded, so a contender waits for the outcome and takes the notice over when the reservation fails. The wait is bounded by PENDING_WAIT_SECONDS, because a notice must never hold up the deprecated operation: a logger that blocks forever costs the contender the notice, not its progress. Deprecations.emitted? now reports notices that actually went out, never one still in flight. Round 8's HTTP+SSE sweep left four places offering the transport as an ordinary choice: the README's Advanced Configuration sse_config example and the auto-detect fallback row, and, in MCPClient.connect's YARD, the "Other HTTP URLs" fallback text and the multiple-server @example, which no longer reaches for an /sse URL at all. The README also offered protocol: and discover_timeout: on MCPClient.connect without qualification, but MCPClient::ServerSSE accepts neither and extract_sse_options drops both. HTTP+SSE has no era of its own and is on the registry's shortest clock, so the documentation now matches the code rather than the code growing era support for a transport on its way out. And the deprecated-features table's Sampling row names the transport-level on_sampling_request trigger round 8 tagged, so the table that claims to list the first-use notices is complete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Round 9's reservation states existed to serve one promise: that a deprecation notice never holds up the deprecated operation. The promise cannot be kept here, and the machinery did not keep it. PENDING_WAIT_SECONDS bounded the CONTENDERS, never the caller actually stuck in the logger. A first use whose logger blocks forever (a synchronous remote logger, stalled I/O) never returned from `warn` at all, so the client construction, SSE connection or incoming request that triggered it never completed either — exactly what the promise said could not happen. And the bounded wait bought the contenders nothing: a reservation that hangs is never released, so every later first use of Sampling, Logging or the HTTP+SSE transport — features whose trigger points are hit repeatedly — waited the timeout out again and still came away without the notice. The alternatives that would keep the stronger promise are both worse. Emitting on a detached thread stops blocking anything, but loses the ordering between the notice and the operation it describes and needs a thread per stalled notice. `Timeout.timeout` around a host's logger raises inside arbitrary third-party code — during its own IO, ensure blocks or internal locking — which is how a logger gets corrupted, not how a caller gets protected. So the promise is narrowed to the one the rest of the library already makes. Every other `logger.warn` in this codebase blocks its caller exactly the same way; a logger that blocks forever blocks the library everywhere, not only in this path, and claiming an exemption here was claiming more for a notice than for any other warning. A notice costs its caller what one `logger.warn` costs it, and no more. The reservation states, the condition variable and the timeout go with the claim. An emission attempt is serialized on a gate held only for the feature being logged: nothing is timed, a caller that meets a notice in flight either finds it emitted (and stands down) or takes it over when that attempt failed, and no caller comes away from a wait with the notice neither emitted nor its own to write. What earlier rounds established stands — one notice per feature per process, a logger that raises or drops warnings neither breaks the deprecated operation nor spends the notice, the logger called outside the module's lock so a notice in flight never delays another feature's notice, `emitted?` or `reset!`, and the bookkeeping (gates included, whose Mutexes a fork could leave locked) rebuilt when the owning PID no longer matches. Round 9's example that pinned the bounded wait now pins what replaced it: a notice never holds up anything but the feature it is logging. New round 10 examples cover the third caller the review asked for — a second and a third first use queued behind a stuck logger both come away with the notice emitted, once, whichever way the stuck one ends — plus the narrowed guarantee in the source and the README. Also: the `ServerHTTP` elicitation note offered `ServerSSE` as an ordinary alternative with no deprecation mark. It now leads with `ServerStreamableHTTP` and marks the HTTP+SSE entry with SEP-2596 and its earliest removal, like the README, `MCPClient.connect` and `sse_config`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Round 10's per-feature emission gate is held across `logger.warn`, which is host code that may itself reach a deprecated feature (a formatter, a log subscriber). A thread already inside a notice that then queued for a gate could wait forever: for its own gate, when the callback warns for the feature being logged, or — through a logger that serializes its writes, as ::Logger does — for a second feature's gate held by a thread waiting on that very logger. Both threads hang, and the sampling request, log level or SSE `connect` behind the notice never returns. A thread inside a notice now never queues: the nested attempt stands down and leaves that notice owed to a later use, exactly as a dropped one is. Round 10's contract survives: one notice per feature per process, and a plain contender still waits for another thread's emission and takes it over when it fails. `Logger.new(nil)` is a supported no-output logger whose level sits below WARN, so the level probe passed it and its `warn` returned successfully having written nothing — spending a notice nobody could read and silencing every later use. A logger with no device is now treated like one that drops warnings: the notice stays owed. MCP 2026-07-28 moved the log level onto every request, so `log_level=` and an incoming `notifications/message` are not the only ways into the deprecated Logging utility: a host that puts `io.modelcontextprotocol/logLevel` in `request_meta` or a per-call `_meta` adopts it too, and `with_request_meta` forwards the key on the wire. That now raises the Logging notice on first use, on the modern path and on the legacy one that passes a supplied `_meta` through untouched. Docs: the README's Protocol Support elicitation bullet no longer offers SSE as an unmarked elicitation transport, and `MCPClient.connect`'s `transport: :sse` entry names the earliest removal beside SEP-2596. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
MCP 2026-07-28 (changelog, minor change 11) removes the notifications/elicitation/complete notification and the elicitationId field of URL-mode elicitation/create requests, both introduced in 2025-11-25. Under the multi round-trip requests pattern the client learns the outcome by retrying the original request, so a server-initiated completion signal and its correlation id no longer fit the protocol; a server that must correlate an elicitation across retries carries its own identifier in the opaque requestState. Client#handle_url_elicitation read params['elicitationId'] unconditionally and passed it to the host's elicitation handler inside the URL-mode metadata hash, for a 2026-07-28 server as well as a 2025-11-25 one. The metadata now follows the asking server's protocol era: a modern server's hash has no elicitationId key at all — not even a nil one — so a non-conforming modern server cannot smuggle a correlation id to the host through it, and one warning names the field (never its value). On a 2025-11-25 server, and when the era was never established, the contract is unchanged, key present (nil when the server sent none) and all. The transports call the elicitation callback with (request_id, params) only, so MCPClient::Client now closes over the asking server when it registers the callback; the multi round-trip path reaches the same callback and is covered too. handle_elicitation_request keeps its two-argument form. This library never implemented notifications/elicitation/complete, so nothing there had to be removed. Also pins the other removed field a server could still push at the client: a modern server that sends Mcp-Session-Id back anyway has it discarded (the only capture point is the initialize response, which a modern server is never sent), never echoed on a later request and never terminated with a DELETE. That held already but nothing exercised it — no stub fed a modern response carrying the header. logging/setLevel, resources/subscribe, resources/unsubscribe, tasks/list and SSE resumability (Last-Event-ID and event ids, including the id: retention on a modern POST stream) were audited in both directions and are sound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Dropping `elicitationId` for a modern server is the protocol decision; the warning that says so is a courtesy. It went out through a bare `@logger.warn` inside the elicitation's rescue, so a logger that raises — or a strict double in a host's own suite — turned a still-valid URL elicitation into a -32603 InputRequiredError and the handler never ran. The notice is now isolated the way MCPClient::Deprecations.warn already isolates its own, and the field is dropped whether or not the notice was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Round 11 kept a thread already inside a notice from queueing for an emission gate. The thread holding the logger's lock need not be inside a notice at all, though: an ordinary `logger.info` holds ::Logger's device lock for the length of the write, and the device — a formatter, a log subscriber, an audit hook — may itself reach a deprecated feature. It then queued for the gate of a second thread that was waiting for the very device lock it held, and neither moved again; the `emitting?` guard could not see it, because that thread was doing ordinary logging. No rule about who may queue fixes it, since a waiter cannot know what it is holding, so the gates are gone. The once-per-process accounting is an atomic claim, taken and released under a mutex this module never holds while calling out, and a caller that meets a notice in flight stands down instead of queueing behind it. Round 9's takeover goes with the wait — a contender no longer writes the notice a failed emission owed — and what survives is what mattered: one notice per feature per process, and a notice that was not written is not spent, so the next use raises it. That also closes the second hole. `Logger#warn` returns true whether it wrote the line or dropped it for its level, and a contender that waited carried a level probe taken before the wait: with the level raised to ERROR in between, its warning was filtered, the process's one notice was marked emitted having been written nowhere, and lowering the level again did not bring it back. The probe now sits next to the write. Coverage. The Roots and Sampling hooks on both SSE transports and the Sampling hook on the multi round-trip path could each be deleted with the focused suite still green; they now have examples that route a request through the transport's own dispatch and assert both the notice and the complete response, and an MRTR exchange that asserts the Sampling notice, both deprecated includeContext values and the retry that carries the answer — all with notices enabled, which the suite otherwise disables. Marking. Counting @deprecated tags cannot show that a given API carries one, and accepting any registry window cannot show that a tag names its own; an explicit API-to-feature inventory replaces both, and named three unmarked surfaces: MCPClient::Root, the Client#roots reader and declare_sampling_tools. Every tag in the library is now accounted for by that inventory. Three weak examples are strengthened too: the DCR disable check resets the notice first (it was silent either way), the sanitization example asserts the emission and the escaped detail, and the URL-mode elicitation example asserts the handler's own answer rather than only that the handler ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The reentrancy guard now covers the level probe, not only the write. `warn` asked `accepts_warnings?` — and so `logger.level` — before `emit_once` put the guard up, so a host whose `level` accessor reaches a deprecated feature recursed until the stack ended. SystemStackError is not a StandardError, so it escaped every rescue meant to keep the feature working: `client.roots = [...]` raised and left the roots unset. The mark goes up in `warn` instead, before the first word to the host, and comes down in its ensure; `emit_once` no longer sets it. It is still a thread-local mark, never a lock, so nothing waits and no lock of this module is held across host code. A closed standard logger no longer eats the notice. `Logger::LogDevice` rescues a write to a closed IO and reports it through `Kernel#warn`, so `Logger#warn` returns exactly as it does after a real write; the notice was marked emitted although nothing was written, and a later use holding a working logger wrote nothing. Absence of a device was the only case asked about, so ask about a closed one too — the notice stays owed. `Client#roots` carries its deprecation in the generated API docs again. The tag sat inside an `@!attribute` directive above a combined `attr_reader`, and YARD drops the docstring of the LAST directive in such a block, re-registering that name from the statement with the leftover empty docstring. YARD published `Client#roots` with no tags at all while every source-text check passed. The readers are declared one per line with ordinary doc comments, which parse — for all six, not just roots. Coverage for the three mutations that survived the previous suite: - the registry's name, revision and SEP are pinned per feature, and against the notice text (Sampling's date could be changed freely); - a modern and a legacy server on one client, both elicitation callbacks invoked in both orders (using the first server's era passed before); - the symbol-keyed roots answer and the legacy `_meta` symbol log-level key, which is the only path that reaches that branch — the modern one stringifies its keys first. Also: the deprecated APIs are now checked through YARD's parsed objects and their tags rather than nearby source text, and the "keeps serving Logging when the logger fails" example drives a server of each era instead of a client with none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The branch below taught plain HTTP to answer a legacy server's request on a response stream instead of leaving it waiting, and reworded this note to say so; this branch reordered the same list to offer Streamable HTTP before the transport on its way out, and marked HTTP+SSE deprecated. The rebase kept both lists. There is one now: the accurate prose, and the ordered list with the deprecation on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Three things this round's reviews found, and the coverage that lets each of them be found only by reading. A logger the host wrapped could spend a notice it never wrote. The level probe asked `logger.level`, which only a bare ::Logger answers, and a host's logger is rarely one: a SimpleDelegator around an ERROR-level Logger forwarded `warn`, returned as if it had written, and marked the process's one notice emitted having printed nothing — silencing the host's own working logger for good. The probe now asks `warn?`, the form a wrapper forwards, of any logger that offers it, and looks through Delegator wrappers for the missing device `Logger.new(nil)` has. A logger implementing `warn` and nothing else is still taken at its word (a strict double answers no `warn?`, and that stays pinned), and asking still cannot fail the deprecated operation: a predicate that raises leaves the notice owed, as a raising `warn` does. A wrapper that filters on something a level cannot express — a tag, a source allow-list — remains beyond reach, and the README and the method say so. The Logging notice did not fire on a transport-direct HTTP+SSE session. It was raised in the routing every other transport shares, but ServerSSE carries no subscriptions/listen stream and parses its own notifications, so the one caller these transport-level notices exist for — a host driving a ServerSSE with `on_notification` — received log messages and was never told. The elicitation section still gave the 2025-11-25 URL-mode hash as universal, so a host reading it looked for `metadata['elicitationId']` on a 2026-07-28 server, found nothing, and correlated against a completion notification the revision removed as well. Both places the README shows that hash now split the two revisions. Coverage, each confirmed against the mutation that motivated it: - `includeContext` "still served" is now read off the params the handler was given, on the transport path and on the Client's five-argument handler; deleting the key after the notice reddens both. - The 2025-11-25 elicitationId now goes through a real ServerStdio's `handle_server_request`, so a strip inside the transport reddens; the same examples negotiate the era AFTER the client registered its callback, and back again, so capturing `modern?` at registration reddens too. - The migration text is pinned to what the registry column tells a host to use instead, not to the production table's agreement with itself; blanking a row reddens. - DCR stays silent for pre-registered credentials and for a Client ID Metadata Document client, and a registration failure survives a notice whose logger raises. Hanging the notice on `get_or_register_client` instead of `register_client` reddens the first two. - Notice-enabled failure paths on both HTTP transports: a sampling handler that raises spends the notice and answers the peer a constant error, a missing handler spends nothing, a roots handler that raises spends nothing, and sampling keeps being served after the notice is gone. - `roots=` still sends `notifications/roots/list_changed` to a legacy server and still spares a modern one, with notices ON — the rest of the suite runs with them off, so nothing else could catch a `roots=` that warned and returned. A Client's configured roots still come back through the modern MRTR path. - A fork while a claim is in flight, and the reentrancy guard across the fibers of one thread: the two bookkeeping edges the module documents and nothing drove. A fiber-local guard, or a child inheriting the claim, redden only these. - Round 9's contended-claim example waited on a 50ms sleep and could fail for being right; it now joins the contender while the claim is provably held. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The four private methods that hook a transport's notifications up and act on them -- dropping caches, running the host's listeners -- are one unit, and moving them keeps Client under its length cop the same way the listing and task mixins already do. Nothing about them changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…ers behind the notices (round 13) Codex round 4 and grok round 4 on PR11 found that every example on the deprecation paths pinned the notice and stopped there: a transport that logged the notice and dropped the peer's response, or a hook that deleted includeContext after reporting it, passed all 211 of them. Round 13 pins: - the stdio transport's sampling response envelope (result, handler error and Internal error) under the request id, params intact, and again once the notice is spent; - includeContext thisServer / allServers / none / absent reaching the sampling handler unchanged on the multi round-trip path; - multi round-trip failures with notices enabled (handler raises, answers an error, returns a non-object; no handler; undeclared sampling.tools) as InputRequiredError with no continuation request; - a complete discovery -> input_required -> continuation exchange for a modern URL elicitation (host metadata without elicitationId, echoed requestState, inputResponses) beside the legacy response envelope; - notifications/elicitation/complete ignored on either era; - the strict-Logger example asserting the notice was written. Grok's one behavioural finding: on the multi round-trip path the undeclared sampling.tools refusal fired before the Sampling and includeContext notices, while the 2025-11-25 server-initiated path warns before its handler rejects the same payload. The notice now precedes the refusal on both eras. The fork examples in rounds 6 and 12 wait with a bound, so a bookkeeping regression fails them instead of wedging the suite; the round 9 contender runs on the example's own thread while the reservation is held instead of hoping a 50ms sleep scheduled it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…out a handler, pin the deprecated operations Review round 5 (codex, grok) on the deprecations branch. A 2025-11-25 sampling/createMessage carrying tools or toolChoice is now refused by the transport itself with -32602 when sampling.tools was never declared (SEP-1577: the client MUST return an error), on the server-initiated path as on the multi round-trip one and after the Sampling/includeContext notices a served-or-refused request owes; a host driving a transport directly no longer depends on its own callback for the check. A request reaching a transport with no sampling handler is answered with -32601 rather than -1, which sampling.mdx reserves for a user rejection. The deprecation notices are pinned as side effects of the operations rather than substitutes for them: the level a notice-enabled client sets reaches the legacy server as logging/setLevel and the modern one on its next request, on the first call and after the notice is spent, and through a logger that writes nothing; deprecated capabilities stay advertised on the first modern request, on the continuation and on the 2025-11-25 handshake; the removed completion notification arriving during an unfinished modern interaction is handed to a generic listener and finishes nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The stateless stdio branch folds a `_meta` written under the Symbol key into the one String-keyed member in every era, so the deprecation notice for a symbol-keyed legacy `_meta` now reads the merged member, and the example pins that the request carries exactly one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The line logged when a tool-enabled sampling/createMessage is refused for an undeclared sampling.tools is written guarded, so a logger that fails on it leaves the SEP-1577 refusal (-32602) on the wire instead of the dispatcher's -32603. The stdio transport's no-handler answer (-32601, no notice spent) and its service of a tool-enabled request once sampling.tools was declared are pinned transport-direct; the completion-notification example observes through the Client's own listener; the roots examples assert the answer that reached the peer. The retry guarantee's boundary is named and pinned: a standard Logger over an open device that fails to write reports it on $stderr only, so that notice is spent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The tasks branch pins that a URL-mode elicitation carrying elicitationId hands the field to the host; the session that example builds is 2026-07-28, and this branch removed the field from the modern contract so a server cannot smuggle a correlation id through it. The example now pins that contract, and the 2025-11-25 one stays pinned in this branch's own specs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
A notice is a side effect of a deprecated operation, never a substitute for it, and four examples asserted only the notice: - The HTTP transports carry their own log_level=, and replacing any of them with a notice-only no-op left the whole deprecation suite green. Each is now driven on both eras: a modern session stores the level and every later request carries it in _meta, a legacy session sends logging/setLevel, and an undefined level is refused before anything goes out. HTTP+SSE, which negotiates no modern revision, is pinned on the request path. - The per-call _meta examples discarded the parameters they built, so they would have passed if the level vanished with the warning that announced it; they now read the level back, and the notification example reads it off the notification. - The modern Roots answer was taken from fulfil_input_requests directly. A scripted stdio exchange now drives discovery, the roots input request and the continuation, pinning that the answer leaves the client under the key the server named with its opaque state echoed. - The detail bound read MAX_DETAIL_LENGTH, so it agreed with whatever the constant said. It spells the documented limit out, pins the 200/201 boundary, and pins that the bound counts escaped characters. Clearing the roots list is use of Roots — the README says every roots= call raises the notice — so that example pins it instead of commenting the opposite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
simonx1
force-pushed
the
mcp-2026/json-schema
branch
from
September 6, 2026 17:04
9893fa4 to
ae786ae
Compare
simonx1
force-pushed
the
mcp-2026/deprecations-docs
branch
from
September 6, 2026 17:06
fa89ee4 to
e440c4d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eleventh (final) PR of the MCP 2026-07-28 series, stacked on #233.
What
MCP 2026-07-28 adopts a feature lifecycle policy and places six features in the Deprecated state (changelog "Deprecated" 1–4: SEP-2577, SEP-2596, PR #2858). They remain fully functional during the deprecation window; new integrations should not adopt them.
MCPClient::Deprecations—REGISTRYnames every deprecated feature with its reference and suggested migration;Deprecations.warn(feature, logger, detail:)logs one notice per feature per process on first use (peer-supplied detail is sanitized and bounded);Deprecations.enabled = falsesilences the notices;reset!/emitted?for tests.roots:at construction andClient#roots=), Sampling (sampling_handler:), Logging (Client#log_level=), the HTTP+SSE transport (ServerSSE.new), and a sampling request carryingincludeContext: "thisServer" | "allServers"(still served, per SEP-2596). Dynamic Client Registration already warned in MCP 2026-07-28: authorization (RFC 9207 iss validation, application_type, AS binding) #232 and is listed in the registry.x-mcp-header,subscriptions/listen, cacheable results, the tasks extension and the authorization changes, and a "Deprecated features" table gives each migration. Roots / Sampling / Logging subsections carry a deprecation note.Behaviour is otherwise unchanged; the notices are disabled for the test suite except in the deprecations spec.
Tests
spec/lib/mcp_client/deprecations_2026_spec.rb(12 examples): registry completeness, one notice per feature per process, silence without the feature, each wiring point, includeContext detail (served, none/absent silent), the disabled switch, unknown feature rejection, log-line forging. Full suite: 2263 examples, 0 failures; rubocop clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7