Skip to main content

rmcp/service/
client.rs

1// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
2#![expect(deprecated)]
3pub(super) mod cache;
4
5use std::{borrow::Cow, num::NonZeroUsize, sync::Arc, time::Duration};
6
7use cache::CacheGeneration;
8pub use cache::{ClientCacheConfig, MAX_CLIENT_CACHE_TTL};
9use thiserror::Error;
10
11use super::*;
12use crate::{
13    model::{
14        ArgumentInfo, CacheScope, CallToolRequest, CallToolRequestParams, CallToolResponse,
15        CallToolResult, CancelTaskParams, CancelTaskRequest, CancelledNotification,
16        CancelledNotificationParam, ClientConfig, ClientJsonRpcMessage, ClientNotification,
17        ClientRequest, ClientResult, CompleteRequest, CompleteRequestParams, CompleteResult,
18        CompletionContext, CompletionInfo, DEFAULT_MRTR_MAX_ROUNDS, DiscoverRequest,
19        DiscoverRequestParams, DiscoverResult, ErrorData, GetExtensions, GetMeta, GetPromptRequest,
20        GetPromptRequestParams, GetPromptResponse, GetPromptResult, GetTaskParams, GetTaskRequest,
21        GetTaskResult, InitializeRequest, InitializeRequestParams, InitializedNotification,
22        InputRequest, InputRequiredResult, InputResponses, JsonRpcResponse, ListPromptsRequest,
23        ListPromptsResult, ListResourceTemplatesRequest, ListResourceTemplatesResult,
24        ListResourcesRequest, ListResourcesResult, ListToolsRequest, ListToolsResult,
25        NumberOrString, PaginatedRequestParams, ProgressNotification, ProgressNotificationParam,
26        ProtocolVersion, ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse,
27        ReadResourceResult, Reference, RequestId, RequestMetaObject, RootsListChangedNotification,
28        ServerJsonRpcMessage, ServerNotification, ServerPeerInfo, ServerRequest, ServerResult,
29        SetLevelRequest, SetLevelRequestParams, SubscribeRequest, SubscribeRequestParams,
30        SubscriptionFilter, SubscriptionsListenRequest, SubscriptionsListenRequestParams,
31        SubscriptionsListenResult, UnsubscribeRequest, UnsubscribeRequestParams, UpdateTaskParams,
32        UpdateTaskRequest,
33    },
34    transport::DynamicTransportError,
35};
36
37/// It represents the error that may occur when serving the client.
38///
39/// if you want to handle the error, you can use `serve_client_with_ct` or `serve_client` with `Result<RunningService<RoleClient, S>, ClientError>`
40#[derive(Error, Debug)]
41#[non_exhaustive]
42pub enum ClientInitializeError {
43    #[error("expect initialized response, but received: {0:?}")]
44    ExpectedInitResponse(Option<ServerJsonRpcMessage>),
45
46    #[error("expect initialized result, but received: {0:?}")]
47    ExpectedInitResult(Option<ServerResult>),
48
49    #[error("conflict initialized response id: expected {0}, got {1}")]
50    ConflictInitResponseId(RequestId, RequestId),
51
52    #[error(
53        "uncorrelated error response: expected id {expected}, error response carried {received}"
54    )]
55    UncorrelatedErrorResponse {
56        expected: RequestId,
57        received: RequestId,
58    },
59
60    #[error("connection closed: {0}")]
61    ConnectionClosed(String),
62
63    #[error("Send message error {error}, when {context}")]
64    TransportError {
65        error: DynamicTransportError,
66        context: Cow<'static, str>,
67    },
68
69    #[error("JSON-RPC error: {0}")]
70    JsonRpcError(ErrorData),
71
72    #[error(
73        "no compatible protocol version (client: {client_supported:?}, server: {server_supported:?})"
74    )]
75    NoCompatibleProtocolVersion {
76        client_supported: Vec<ProtocolVersion>,
77        server_supported: Vec<ProtocolVersion>,
78    },
79
80    #[error("discover startup requires at least one preferred protocol version")]
81    NoPreferredProtocolVersion,
82
83    #[error("Cancelled")]
84    Cancelled,
85
86    #[error("discover and legacy initialize both failed")]
87    LegacyFallbackFailed {
88        discover: Box<ClientInitializeError>,
89        fallback: Box<ClientInitializeError>,
90    },
91}
92
93impl ClientInitializeError {
94    pub fn transport<T: Transport<RoleClient> + 'static>(
95        error: T::Error,
96        context: impl Into<Cow<'static, str>>,
97    ) -> Self {
98        Self::TransportError {
99            error: DynamicTransportError::new::<T, _>(error),
100            context: context.into(),
101        }
102    }
103
104    /// The `WWW-Authenticate` challenge from the 401/403 the transport hit
105    /// during initialization, if that is why initialization failed.
106    ///
107    /// This is the trigger of the reactive OAuth flow: feed the challenge to
108    /// `AuthorizationRequest::with_challenge` to authorize, then reconnect.
109    #[cfg(feature = "transport-streamable-http-client")]
110    pub fn auth_challenge(&self) -> Option<&str> {
111        use crate::transport::streamable_http_client::{AuthRequiredError, InsufficientScopeError};
112
113        let error = match self {
114            Self::TransportError { error, .. } => error,
115            // A 401/403 in the fallback phase is still actionable.
116            Self::LegacyFallbackFailed { fallback, .. } => {
117                return fallback.auth_challenge();
118            }
119            _ => return None,
120        };
121        let mut source: Option<&(dyn std::error::Error + 'static)> = Some(error.error.as_ref());
122        while let Some(current) = source {
123            if let Some(auth_required) = current.downcast_ref::<AuthRequiredError>() {
124                return Some(&auth_required.www_authenticate_header);
125            }
126            if let Some(insufficient_scope) = current.downcast_ref::<InsufficientScopeError>() {
127                return Some(&insufficient_scope.www_authenticate_header);
128            }
129            source = current.source();
130        }
131        None
132    }
133
134    /// Returns whether client initialization failed because authorization is required.
135    ///
136    /// This covers both missing or expired local OAuth authorization and an HTTP
137    /// authorization challenge from the MCP server.
138    pub fn is_authorization_required(&self) -> bool {
139        match self {
140            Self::TransportError { error, .. } => error.is_authorization_required(),
141            Self::LegacyFallbackFailed { fallback, .. } => fallback.is_authorization_required(),
142            _ => false,
143        }
144    }
145}
146
147/// Helper function to get the next message from the stream
148async fn expect_next_message<T>(
149    transport: &mut T,
150    context: &str,
151) -> Result<ServerJsonRpcMessage, ClientInitializeError>
152where
153    T: Transport<RoleClient>,
154{
155    transport
156        .receive()
157        .await
158        .ok_or_else(|| ClientInitializeError::ConnectionClosed(context.to_string()))
159}
160
161/// Whether `expect_response` answers a server ping that arrives before the
162/// handshake completes.
163#[derive(Clone, Copy)]
164enum PreInitPing {
165    Answer,
166    Ignore,
167}
168
169/// Helper function to expect a response from the stream, correlated to
170/// `expected_id`.
171///
172/// Both success and error responses are checked here: a mismatched id on a
173/// success response is `ConflictInitResponseId`; on an error response (whose
174/// `id` is optional per spec) it is `UncorrelatedErrorResponse`. The caller
175/// never sees an uncorrelated response.
176///
177/// A server ping is answered only with `PreInitPing::Answer`. The discover
178/// wait, including Auto's timeout-bounded probe, leaves it unanswered:
179/// 2026-07-28 has no ping, and a cancelled send could leave a partial frame.
180async fn expect_response<T, S>(
181    transport: &mut T,
182    context: &str,
183    service: &S,
184    peer: Peer<RoleClient>,
185    expected_id: &RequestId,
186    pre_init_ping: PreInitPing,
187) -> Result<ServerResult, ClientInitializeError>
188where
189    T: Transport<RoleClient> + 'static,
190    S: Service<RoleClient>,
191{
192    loop {
193        let message = expect_next_message(transport, context).await?;
194        match message {
195            ServerJsonRpcMessage::Response(JsonRpcResponse { id, result, .. }) => {
196                if !expected_id.matches_response_id(&id) {
197                    return Err(ClientInitializeError::ConflictInitResponseId(
198                        expected_id.clone(),
199                        id,
200                    ));
201                }
202                return Ok(result);
203            }
204            ServerJsonRpcMessage::Error(error) => {
205                return Err(match &error.id {
206                    Some(id) if expected_id.matches_response_id(id) => {
207                        ClientInitializeError::JsonRpcError(error.error)
208                    }
209                    // Spec: error id is optional; a server that cannot read
210                    // the request id omits it. The error is still a response
211                    // to our request, so it remains available to the caller.
212                    None => ClientInitializeError::JsonRpcError(error.error),
213                    Some(id) => ClientInitializeError::UncorrelatedErrorResponse {
214                        expected: expected_id.clone(),
215                        received: id.clone(),
216                    },
217                });
218            }
219            // Server could send logging messages before handshake
220            ServerJsonRpcMessage::Notification(mut notification) => {
221                let ServerNotification::LoggingMessageNotification(logging) =
222                    &mut notification.notification
223                else {
224                    tracing::warn!(?notification, "Received unexpected message");
225                    continue;
226                };
227
228                let mut context = NotificationContext {
229                    peer: peer.clone(),
230                    meta: NotificationMetaObject::default(),
231                    extensions: Extensions::default(),
232                };
233
234                if let Some(meta) = logging.extensions.get_mut::<NotificationMetaObject>() {
235                    std::mem::swap(&mut context.meta, meta);
236                }
237                std::mem::swap(&mut context.extensions, &mut logging.extensions);
238
239                if let Err(error) = service
240                    .handle_notification(notification.notification, context)
241                    .await
242                {
243                    tracing::warn!(?error, "Handle logging before handshake failed.");
244                }
245            }
246            // Server could send pings before handshake
247            ServerJsonRpcMessage::Request(request)
248                if matches!(request.request, ServerRequest::PingRequest(_)) =>
249            {
250                match pre_init_ping {
251                    PreInitPing::Answer => {
252                        transport
253                            .send(ClientJsonRpcMessage::response(
254                                ClientResult::empty(()),
255                                request.id,
256                            ))
257                            .await
258                            .map_err(|error| {
259                                ClientInitializeError::transport::<T>(
260                                    error,
261                                    "send pre-init ping response",
262                                )
263                            })?;
264                    }
265                    PreInitPing::Ignore => tracing::trace!("Received ping request. Ignored."),
266                }
267            }
268            // Server SHOULD NOT send any other messages before handshake. We ignore them anyway
269            _ => tracing::warn!(?message, "Received unexpected message"),
270        }
271    }
272}
273
274#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
275#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
276pub struct RoleClient;
277
278/// Select the first client-preferred protocol version supported by the server.
279///
280/// Returns `None` when no version is shared.
281pub fn select_protocol_version(
282    client_preference: &[ProtocolVersion],
283    server_supported: &[ProtocolVersion],
284) -> Option<ProtocolVersion> {
285    client_preference
286        .iter()
287        .find(|version| server_supported.contains(version))
288        .cloned()
289}
290
291impl ServiceRole for RoleClient {
292    type Req = ClientRequest;
293    type Resp = ClientResult;
294    type Not = ClientNotification;
295    type PeerReq = ServerRequest;
296    type PeerResp = ServerResult;
297    type PeerNot = ServerNotification;
298    type Info = ClientConfig;
299    type PeerInfo = ServerPeerInfo;
300    type InitializeError = ClientInitializeError;
301    const IS_CLIENT: bool = true;
302
303    fn configure_direct_peer(peer: &Peer<Self>, info: &Self::Info) {
304        let Some(server_info) = peer.peer_info() else {
305            return;
306        };
307        if server_info.protocol_version.has_initialize() {
308            return;
309        }
310        peer.set_client_request_metadata(ClientRequestMetadata {
311            protocol_version: server_info.protocol_version.clone(),
312            client_info: info.client_info.clone(),
313            client_capabilities: info.capabilities.clone(),
314        });
315    }
316
317    fn peer_cancelled_params(notification: &Self::PeerNot) -> Option<&CancelledNotificationParam> {
318        match notification {
319            ServerNotification::CancelledNotification(notification) => Some(&notification.params),
320            _ => None,
321        }
322    }
323
324    fn peer_cancels_subscriptions(peer: &Peer<Self>) -> bool {
325        // Discovery keeps modern lifecycle semantics even with an older application version.
326        !super::uses_legacy_lifecycle(
327            peer.peer_info()
328                .as_deref()
329                .map(|info| &info.protocol_version),
330            peer.client_request_metadata.get().is_some(),
331        )
332    }
333
334    fn is_subscription_request(request: &Self::Req) -> bool {
335        matches!(request, ClientRequest::SubscriptionsListenRequest(_))
336    }
337
338    // SEP-2260: reject restricted server requests that arrived unassociated
339    // with any in-flight outbound request. Without stream separation
340    // (`Unknown`) the coarse in-flight check under-approximates the SHOULD.
341    fn enforce_peer_request_association(
342        peer_request: &Self::PeerReq,
343        peer_info: Option<&Self::PeerInfo>,
344        association: PeerRequestAssociation,
345    ) -> Result<(), ErrorData> {
346        let restricted = matches!(
347            peer_request,
348            ServerRequest::CreateMessageRequest(_)
349                | ServerRequest::ListRootsRequest(_)
350                | ServerRequest::ElicitRequest(_)
351        );
352        if !restricted {
353            return Ok(());
354        }
355        let strict =
356            peer_info.is_some_and(|info| info.protocol_version >= ProtocolVersion::V_2026_07_28);
357        if !strict {
358            return Ok(());
359        }
360        let associated = match association {
361            PeerRequestAssociation::Associated => true,
362            PeerRequestAssociation::Unassociated => false,
363            PeerRequestAssociation::Unknown {
364                has_pending_outbound_request,
365            } => has_pending_outbound_request,
366        };
367        if associated {
368            Ok(())
369        } else {
370            Err(ErrorData::invalid_params(
371                "SEP-2260: server-to-client requests must be associated with an in-flight client request",
372                None,
373            ))
374        }
375    }
376
377    async fn invalidate_response_cache(peer: &Peer<Self>, notification: &Self::PeerNot) {
378        match notification {
379            ServerNotification::ResourceUpdatedNotification(notification) => {
380                peer.invalidate_resource_read_cache(&notification.params.uri)
381                    .await;
382            }
383            ServerNotification::ResourceListChangedNotification(_) => {
384                peer.invalidate_resource_list_cache().await;
385            }
386            ServerNotification::ToolListChangedNotification(_) => {
387                peer.invalidate_tool_cache().await;
388            }
389            ServerNotification::PromptListChangedNotification(_) => {
390                peer.invalidate_prompt_cache().await;
391            }
392            _ => {}
393        }
394    }
395}
396
397pub type ServerSink = Peer<RoleClient>;
398
399/// Default number of notifications buffered for one subscription.
400pub const DEFAULT_SUBSCRIPTION_CHANNEL_CAPACITY: usize = 64;
401
402/// How a client-side subscription stream ended.
403#[derive(Debug, Clone, PartialEq)]
404#[non_exhaustive]
405pub enum SubscriptionEnd {
406    /// The server returned a final `SubscriptionsListenResult`.
407    Graceful(SubscriptionsListenResult),
408    /// The transport closed without a final result. Call `Peer::listen` again
409    /// after reconnecting; subscription streams are not resumable.
410    Abrupt,
411    /// The subscription was explicitly cancelled by either peer.
412    Cancelled,
413    /// The consumer did not drain notifications before the channel filled.
414    Lagged { capacity: usize },
415}
416
417/// Handle for one active `subscriptions/listen` request.
418#[derive(Debug)]
419pub struct Subscription {
420    id: RequestId,
421    acknowledged: SubscriptionFilter,
422    notifications: tokio::sync::mpsc::Receiver<ServerNotification>,
423    request: Option<RequestHandle<RoleClient>>,
424    end: Option<SubscriptionEnd>,
425}
426
427type SubscriptionResponse =
428    Result<Result<ServerResult, ServiceError>, tokio::sync::oneshot::error::RecvError>;
429
430struct PendingSubscriptionRequest {
431    handle: Option<RequestHandle<RoleClient>>,
432}
433
434impl PendingSubscriptionRequest {
435    fn new(handle: RequestHandle<RoleClient>) -> Self {
436        Self {
437            handle: Some(handle),
438        }
439    }
440
441    async fn recv(&mut self) -> Option<SubscriptionResponse> {
442        let handle = self.handle.as_mut()?;
443        Some((&mut handle.rx).await)
444    }
445
446    fn take(&mut self) -> Option<RequestHandle<RoleClient>> {
447        self.handle.take()
448    }
449
450    fn unregister(&self, id: &RequestId) {
451        if let Some(handle) = self.handle.as_ref() {
452            handle.peer.unregister_subscription(id);
453        }
454    }
455
456    fn disarm(&mut self) {
457        self.handle.take();
458    }
459
460    async fn cancel(&mut self, reason: &'static str) {
461        if let Some(handle) = self.handle.take() {
462            let _ = handle.cancel(Some(reason.to_owned())).await;
463        }
464    }
465}
466
467impl Drop for PendingSubscriptionRequest {
468    fn drop(&mut self) {
469        let Some(handle) = self.handle.take() else {
470            return;
471        };
472        handle.peer.unregister_subscription(&handle.id);
473        handle.peer.try_cancel_request(
474            handle.id,
475            Some("subscription establishment cancelled".to_owned()),
476        );
477    }
478}
479
480/// Tells the server about a request whose caller stopped waiting for it.
481///
482/// Armed while the response is awaited. Dropping it armed, which happens when
483/// the caller drops the future (a `tokio::time::timeout`, a lost `select!`
484/// branch), sends a best-effort `notifications/cancelled` for the request, so
485/// the server does not keep working on a result nobody will read.
486struct AbandonedRequestGuard<'a> {
487    peer: &'a Peer<RoleClient>,
488    id: Option<RequestId>,
489}
490
491impl<'a> AbandonedRequestGuard<'a> {
492    const REASON: &'static str = "request abandoned by the caller";
493
494    fn new(peer: &'a Peer<RoleClient>, id: RequestId) -> Self {
495        Self { peer, id: Some(id) }
496    }
497
498    fn disarm(&mut self) {
499        self.id.take();
500    }
501}
502
503impl Drop for AbandonedRequestGuard<'_> {
504    fn drop(&mut self) {
505        let Some(id) = self.id.take() else {
506            return;
507        };
508        self.peer
509            .try_cancel_request(id, Some(Self::REASON.to_owned()));
510    }
511}
512
513impl Subscription {
514    /// Return the originating listen request ID.
515    pub fn id(&self) -> &RequestId {
516        &self.id
517    }
518
519    /// Return the notification filter accepted by the server.
520    pub fn acknowledged(&self) -> &SubscriptionFilter {
521        &self.acknowledged
522    }
523
524    /// Return the terminal state after this subscription has ended.
525    pub fn end(&self) -> Option<&SubscriptionEnd> {
526        self.end.as_ref()
527    }
528
529    /// Receive the next notification, or `None` after the subscription ends.
530    ///
531    /// A graceful final result and an abrupt transport close are distinguished
532    /// through [`Self::end`].
533    ///
534    /// # Errors
535    ///
536    /// Returns a service or protocol error when the stream carries an invalid
537    /// message, an unexpected final result, or another request failure.
538    pub async fn next(&mut self) -> Result<Option<ServerNotification>, ServiceError> {
539        if self.end.is_some() {
540            return Ok(None);
541        }
542        let Some(request) = self.request.as_mut() else {
543            self.end = Some(SubscriptionEnd::Abrupt);
544            return Ok(None);
545        };
546
547        tokio::select! {
548            biased;
549            notification = self.notifications.recv() => {
550                let Some(notification) = notification else {
551                    let response = (&mut request.rx).await;
552                    return self.handle_response(response);
553                };
554                if let ServerNotification::CancelledNotification(cancelled) = &notification {
555                    if cancelled.params.request_id.as_ref() != Some(&self.id) {
556                        self.cancel_as_abrupt("subscription cancellation ID mismatch")
557                            .await;
558                        return Err(ServiceError::UnexpectedResponse);
559                    }
560                    self.finish(SubscriptionEnd::Cancelled);
561                    return Ok(None);
562                }
563                if notification.get_meta().subscription_id().as_ref() != Some(&self.id) {
564                    self.cancel_as_abrupt("subscription notification ID mismatch")
565                        .await;
566                    return Err(ServiceError::UnexpectedResponse);
567                }
568                if !self.accepts(&notification) {
569                    self.cancel_as_abrupt(
570                        "subscription notification was outside the acknowledged filter",
571                    )
572                    .await;
573                    return Err(ServiceError::UnexpectedResponse);
574                }
575                Ok(Some(notification))
576            }
577            response = &mut request.rx => {
578                self.handle_response(response)
579            }
580        }
581    }
582
583    /// Cancel this subscription.
584    ///
585    /// # Errors
586    ///
587    /// Returns a transport error when the cancellation signal cannot be sent.
588    pub async fn cancel(&mut self) -> Result<(), ServiceError> {
589        self.cancel_with_reason(None).await
590    }
591
592    /// Cancel this subscription with a diagnostic reason.
593    ///
594    /// # Errors
595    ///
596    /// Returns a transport error when the cancellation signal cannot be sent.
597    pub async fn cancel_with_reason(&mut self, reason: Option<String>) -> Result<(), ServiceError> {
598        let Some(request) = self.request.take() else {
599            return Ok(());
600        };
601        request.cancel(reason).await?;
602        self.end = Some(SubscriptionEnd::Cancelled);
603        Ok(())
604    }
605
606    fn finish(&mut self, end: SubscriptionEnd) {
607        if let Some(request) = self.request.take() {
608            request.peer.unregister_subscription(&self.id);
609        }
610        self.end = Some(end);
611    }
612
613    async fn cancel_as_abrupt(&mut self, reason: &'static str) {
614        if let Some(request) = self.request.take() {
615            let _ = request.cancel(Some(reason.to_owned())).await;
616        }
617        self.end = Some(SubscriptionEnd::Abrupt);
618    }
619
620    fn accepts(&self, notification: &ServerNotification) -> bool {
621        match notification {
622            ServerNotification::ToolListChangedNotification(_) => {
623                self.acknowledged.tools_list_changed == Some(true)
624            }
625            ServerNotification::PromptListChangedNotification(_) => {
626                self.acknowledged.prompts_list_changed == Some(true)
627            }
628            ServerNotification::ResourceListChangedNotification(_) => {
629                self.acknowledged.resources_list_changed == Some(true)
630            }
631            ServerNotification::ResourceUpdatedNotification(update) => self
632                .acknowledged
633                .resource_subscriptions
634                .as_ref()
635                .is_some_and(|uris| uris.contains(&update.params.uri)),
636            ServerNotification::SubscriptionsAcknowledgedNotification(_)
637            | ServerNotification::CancelledNotification(_)
638            | ServerNotification::ProgressNotification(_)
639            | ServerNotification::LoggingMessageNotification(_)
640            | ServerNotification::TaskStatusNotification(_)
641            | ServerNotification::CustomNotification(_) => false,
642        }
643    }
644
645    fn handle_response(
646        &mut self,
647        response: SubscriptionResponse,
648    ) -> Result<Option<ServerNotification>, ServiceError> {
649        let response = match response {
650            Ok(response) => response,
651            Err(_) => {
652                self.finish(SubscriptionEnd::Abrupt);
653                return Ok(None);
654            }
655        };
656        let response = match response {
657            Ok(response) => response,
658            Err(ServiceError::TransportClosed) => {
659                self.finish(SubscriptionEnd::Abrupt);
660                return Ok(None);
661            }
662            Err(ServiceError::SubscriptionLagged { capacity }) => {
663                self.finish(SubscriptionEnd::Lagged { capacity });
664                return Ok(None);
665            }
666            Err(error) => {
667                self.finish(SubscriptionEnd::Abrupt);
668                return Err(error);
669            }
670        };
671        let ServerResult::SubscriptionsListenResult(result) = response else {
672            self.finish(SubscriptionEnd::Abrupt);
673            return Err(ServiceError::UnexpectedResponse);
674        };
675        if !result.result_type.is_complete()
676            || result.meta.subscription_id().as_ref() != Some(&self.id)
677        {
678            self.finish(SubscriptionEnd::Abrupt);
679            return Err(ServiceError::UnexpectedResponse);
680        }
681        self.finish(SubscriptionEnd::Graceful(result));
682        Ok(None)
683    }
684}
685
686impl Drop for Subscription {
687    fn drop(&mut self) {
688        let Some(request) = self.request.take() else {
689            return;
690        };
691        request.peer.unregister_subscription(&self.id);
692        request.peer.try_cancel_request(
693            self.id.clone(),
694            Some("subscription handle dropped".to_owned()),
695        );
696    }
697}
698
699/// Selects how a client establishes its MCP lifecycle.
700///
701/// Existing [`ServiceExt::serve`] behavior remains legacy initialization.
702#[derive(Debug, Clone, PartialEq, Eq)]
703#[non_exhaustive]
704pub enum ClientLifecycleMode {
705    /// Use the legacy `initialize` / `notifications/initialized` handshake.
706    Initialize,
707    /// Use `server/discover` and send self-contained per-request metadata.
708    Discover {
709        preferred_versions: Vec<ProtocolVersion>,
710    },
711    /// Probe with `server/discover`, falling back when the peer reports that it is legacy or does
712    /// not respond within 10 seconds.
713    Auto {
714        preferred_versions: Vec<ProtocolVersion>,
715        legacy_version: Option<ProtocolVersion>,
716    },
717}
718
719const DEFAULT_AUTO_DISCOVER_TIMEOUT: Duration = Duration::from_secs(10);
720
721/// Client-specific lifecycle entry points.
722pub trait ClientServiceExt: Service<RoleClient> + Sized {
723    fn serve_with_lifecycle<T, E, A>(
724        self,
725        transport: T,
726        lifecycle: ClientLifecycleMode,
727    ) -> impl Future<Output = Result<RunningService<RoleClient, Self>, ClientInitializeError>>
728    + MaybeSendFuture
729    where
730        T: IntoTransport<RoleClient, E, A>,
731        E: std::error::Error + Send + Sync + 'static,
732    {
733        serve_client_with_lifecycle(self, transport, lifecycle)
734    }
735}
736
737impl<S: Service<RoleClient>> ClientServiceExt for S {}
738
739impl<S: Service<RoleClient>> ServiceExt<RoleClient> for S {
740    fn serve_with_ct<T, E, A>(
741        self,
742        transport: T,
743        ct: CancellationToken,
744    ) -> impl Future<Output = Result<RunningService<RoleClient, Self>, ClientInitializeError>>
745    + MaybeSendFuture
746    where
747        T: IntoTransport<RoleClient, E, A>,
748        E: std::error::Error + Send + Sync + 'static,
749        Self: Sized,
750    {
751        serve_client_with_ct(self, transport, ct)
752    }
753}
754
755pub async fn serve_client<S, T, E, A>(
756    service: S,
757    transport: T,
758) -> Result<RunningService<RoleClient, S>, ClientInitializeError>
759where
760    S: Service<RoleClient>,
761    T: IntoTransport<RoleClient, E, A>,
762    E: std::error::Error + Send + Sync + 'static,
763{
764    serve_client_with_lifecycle_and_ct(
765        service,
766        transport,
767        ClientLifecycleMode::Initialize,
768        Default::default(),
769    )
770    .await
771}
772
773pub async fn serve_client_with_ct<S, T, E, A>(
774    service: S,
775    transport: T,
776    ct: CancellationToken,
777) -> Result<RunningService<RoleClient, S>, ClientInitializeError>
778where
779    S: Service<RoleClient>,
780    T: IntoTransport<RoleClient, E, A>,
781    E: std::error::Error + Send + Sync + 'static,
782{
783    serve_client_with_lifecycle_and_ct(service, transport, ClientLifecycleMode::Initialize, ct)
784        .await
785}
786
787pub async fn serve_client_with_lifecycle<S, T, E, A>(
788    service: S,
789    transport: T,
790    lifecycle: ClientLifecycleMode,
791) -> Result<RunningService<RoleClient, S>, ClientInitializeError>
792where
793    S: Service<RoleClient>,
794    T: IntoTransport<RoleClient, E, A>,
795    E: std::error::Error + Send + Sync + 'static,
796{
797    serve_client_with_lifecycle_and_ct(service, transport, lifecycle, Default::default()).await
798}
799
800pub async fn serve_client_with_lifecycle_and_ct<S, T, E, A>(
801    service: S,
802    transport: T,
803    lifecycle: ClientLifecycleMode,
804    ct: CancellationToken,
805) -> Result<RunningService<RoleClient, S>, ClientInitializeError>
806where
807    S: Service<RoleClient>,
808    T: IntoTransport<RoleClient, E, A>,
809    E: std::error::Error + Send + Sync + 'static,
810{
811    tokio::select! {
812        result = serve_client_with_ct_inner(
813            service,
814            transport.into_transport(),
815            lifecycle,
816            ct.clone(),
817            DEFAULT_AUTO_DISCOVER_TIMEOUT,
818        ) => { result }
819        _ = ct.cancelled() => {
820            Err(ClientInitializeError::Cancelled)
821        }
822    }
823}
824
825async fn serve_client_with_ct_inner<S, T>(
826    service: S,
827    transport: T,
828    lifecycle: ClientLifecycleMode,
829    ct: CancellationToken,
830    auto_discover_timeout: Duration,
831) -> Result<RunningService<RoleClient, S>, ClientInitializeError>
832where
833    S: Service<RoleClient>,
834    T: Transport<RoleClient> + 'static,
835{
836    let mut transport = transport.into_transport();
837    let id_provider = <Arc<AtomicU32RequestIdProvider>>::default();
838    let (peer, peer_rx) = Peer::new(id_provider.clone(), None);
839    let client_info = service.get_info();
840
841    match lifecycle {
842        ClientLifecycleMode::Initialize => {
843            legacy_startup(&service, &mut transport, &id_provider, &peer, client_info).await?;
844        }
845        ClientLifecycleMode::Discover { preferred_versions } => {
846            match discover_startup(
847                &service,
848                &mut transport,
849                &id_provider,
850                &peer,
851                &client_info,
852                preferred_versions,
853            )
854            .await?
855            {
856                DiscoverOutcome::Modern => {}
857                // Discover mode does not fall back; a legacy server is an error.
858                DiscoverOutcome::Legacy(error) => return Err(*error),
859            }
860        }
861        ClientLifecycleMode::Auto {
862            preferred_versions,
863            legacy_version,
864        } => {
865            let discover_result = tokio::time::timeout(
866                auto_discover_timeout,
867                discover_startup(
868                    &service,
869                    &mut transport,
870                    &id_provider,
871                    &peer,
872                    &client_info,
873                    preferred_versions,
874                ),
875            )
876            .await;
877            match discover_result {
878                Ok(Ok(DiscoverOutcome::Modern)) => {}
879                Ok(Ok(DiscoverOutcome::Legacy(discover_error))) => {
880                    let mut legacy_info = client_info;
881                    if let Some(version) = legacy_version {
882                        legacy_info.protocol_version = version;
883                    }
884                    if let Err(fallback_error) =
885                        legacy_startup(&service, &mut transport, &id_provider, &peer, legacy_info)
886                            .await
887                    {
888                        return Err(ClientInitializeError::LegacyFallbackFailed {
889                            discover: discover_error,
890                            fallback: Box::new(fallback_error),
891                        });
892                    }
893                }
894                Ok(Err(error)) => return Err(error),
895                Err(_) => {
896                    let mut legacy_info = client_info;
897                    if let Some(version) = legacy_version {
898                        legacy_info.protocol_version = version;
899                    }
900                    legacy_startup(&service, &mut transport, &id_provider, &peer, legacy_info)
901                        .await?;
902                }
903            }
904        }
905    }
906    Ok(serve_inner(
907        service,
908        transport,
909        peer,
910        peer_rx,
911        VecDeque::new(),
912        ct,
913    ))
914}
915
916/// Modern-era JSON-RPC error codes a server can return from `server/discover`
917/// without being legacy. Version negotiation (`UNSUPPORTED_PROTOCOL_VERSION`)
918/// is handled by `discover_startup`'s own retry loop and never reaches the
919/// classification below.
920///
921/// `ErrorCode` is an open integer type, so this cannot be exhaustive: if a
922/// future revision adds another modern-era rejection code, add it here.
923fn is_modern_rejection_code(code: crate::model::ErrorCode) -> bool {
924    matches!(
925        code,
926        crate::model::ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY
927            | crate::model::ErrorCode::HEADER_MISMATCH
928    )
929}
930
931/// The outcome of a `server/discover` probe, classified at the point where all
932/// the context (request id, response correlation, transport state) is still
933/// available.
934///
935/// `Legacy` is returned only when the probe produced a complete, correlated
936/// JSON-RPC error whose code is not a modern-era rejection — i.e. the
937/// transport is in a known-good state and the error identifies the peer as
938/// legacy per the 2026-07-28 backward-compatibility guidance. Every other
939/// failure (transport error, uncorrelated response, modern rejection, etc.)
940/// becomes `Err` so the caller surfaces it instead of retrying.
941enum DiscoverOutcome {
942    /// The server speaks the modern protocol; discovery succeeded.
943    Modern,
944    /// The server is legacy: discovery received a correlated, non-modern
945    /// JSON-RPC error. The transport is still usable for a legacy `initialize`
946    /// handshake. The original error is preserved so a failed fallback can
947    /// report both phases.
948    Legacy(Box<ClientInitializeError>),
949}
950
951async fn legacy_startup<S, T>(
952    service: &S,
953    transport: &mut T,
954    id_provider: &Arc<AtomicU32RequestIdProvider>,
955    peer: &Peer<RoleClient>,
956    client_info: InitializeRequestParams,
957) -> Result<(), ClientInitializeError>
958where
959    S: Service<RoleClient>,
960    T: Transport<RoleClient> + 'static,
961{
962    let id = id_provider.next_request_id();
963    let client_info_for_meta = client_info.clone();
964    let init_request = InitializeRequest {
965        method: Default::default(),
966        params: client_info,
967        extensions: Default::default(),
968    };
969    transport
970        .send(ClientJsonRpcMessage::request(
971            ClientRequest::InitializeRequest(init_request),
972            id.clone(),
973        ))
974        .await
975        .map_err(|error| ClientInitializeError::TransportError {
976            error: DynamicTransportError::new::<T, _>(error),
977            context: "send initialize request".into(),
978        })?;
979
980    let response = expect_response(
981        transport,
982        "initialize response",
983        service,
984        peer.clone(),
985        &id,
986        PreInitPing::Answer,
987    )
988    .await?;
989
990    let ServerResult::InitializeResult(initialize_result) = response else {
991        return Err(ClientInitializeError::ExpectedInitResult(Some(response)));
992    };
993    // A server may answer `initialize` with a version that has no handshake
994    // (e.g. a client pinned past `NO_INITIALIZE` talking to a server that
995    // honors it). From there the lifecycle lives in per-request `_meta`, so the
996    // metadata has to be seeded now or every later request omits it.
997    if !initialize_result.protocol_version.has_initialize() {
998        peer.set_client_request_metadata(ClientRequestMetadata {
999            protocol_version: initialize_result.protocol_version.clone(),
1000            client_info: client_info_for_meta.client_info,
1001            client_capabilities: client_info_for_meta.capabilities,
1002        });
1003    }
1004    peer.set_peer_info(initialize_result.into());
1005
1006    // send notification
1007    let notification = ClientJsonRpcMessage::notification(
1008        ClientNotification::InitializedNotification(InitializedNotification {
1009            method: Default::default(),
1010            extensions: Default::default(),
1011        }),
1012    );
1013    transport.send(notification).await.map_err(|error| {
1014        ClientInitializeError::transport::<T>(error, "send initialized notification")
1015    })?;
1016    Ok(())
1017}
1018
1019async fn discover_startup<S, T>(
1020    service: &S,
1021    transport: &mut T,
1022    id_provider: &Arc<AtomicU32RequestIdProvider>,
1023    peer: &Peer<RoleClient>,
1024    client_info: &InitializeRequestParams,
1025    preferred_versions: Vec<ProtocolVersion>,
1026) -> Result<DiscoverOutcome, ClientInitializeError>
1027where
1028    S: Service<RoleClient>,
1029    T: Transport<RoleClient> + 'static,
1030{
1031    if preferred_versions.is_empty() {
1032        return Err(ClientInitializeError::NoPreferredProtocolVersion);
1033    }
1034
1035    let mut attempted = Vec::new();
1036    let mut candidate = preferred_versions[0].clone();
1037    loop {
1038        attempted.push(candidate.clone());
1039
1040        let meta = RequestMetaObject::with_client_context(
1041            candidate.clone(),
1042            client_info.client_info.clone(),
1043            client_info.capabilities.clone(),
1044        );
1045        let mut discover = DiscoverRequest::new(DiscoverRequestParams {});
1046        discover.extensions.insert(meta);
1047        let id = id_provider.next_request_id();
1048        transport
1049            .send(ClientJsonRpcMessage::request(
1050                ClientRequest::DiscoverRequest(discover),
1051                id.clone(),
1052            ))
1053            .await
1054            .map_err(|error| {
1055                ClientInitializeError::transport::<T>(error, "send discover request")
1056            })?;
1057
1058        match expect_response(
1059            transport,
1060            "discover response",
1061            service,
1062            peer.clone(),
1063            &id,
1064            PreInitPing::Ignore,
1065        )
1066        .await
1067        {
1068            Ok(ServerResult::DiscoverResult(result)) => {
1069                let Some(selected) =
1070                    select_protocol_version(&preferred_versions, &result.supported_versions)
1071                else {
1072                    return Err(ClientInitializeError::NoCompatibleProtocolVersion {
1073                        client_supported: preferred_versions,
1074                        server_supported: result.supported_versions,
1075                    });
1076                };
1077                peer.set_peer_info(ServerPeerInfo::from_discover_result(
1078                    selected.clone(),
1079                    result,
1080                ));
1081                peer.set_client_request_metadata(ClientRequestMetadata {
1082                    protocol_version: selected,
1083                    client_info: client_info.client_info.clone(),
1084                    client_capabilities: client_info.capabilities.clone(),
1085                });
1086                return Ok(DiscoverOutcome::Modern);
1087            }
1088            Ok(response) => {
1089                return Err(ClientInitializeError::ExpectedInitResult(Some(response)));
1090            }
1091            Err(ClientInitializeError::JsonRpcError(error))
1092                if error.code == crate::model::ErrorCode::UNSUPPORTED_PROTOCOL_VERSION =>
1093            {
1094                let supported = error
1095                    .data
1096                    .as_ref()
1097                    .and_then(|data| data.get("supported"))
1098                    .cloned()
1099                    .and_then(|value| serde_json::from_value::<Vec<ProtocolVersion>>(value).ok())
1100                    .unwrap_or_default();
1101                let may_retry_current = attempted
1102                    .iter()
1103                    .filter(|version| *version == &candidate)
1104                    .count()
1105                    == 1;
1106                let next = preferred_versions
1107                    .iter()
1108                    .find(|version| {
1109                        supported.contains(version)
1110                            && (!attempted.contains(version)
1111                                || (may_retry_current && *version == &candidate))
1112                    })
1113                    .cloned();
1114                let Some(next) = next else {
1115                    return Err(ClientInitializeError::NoCompatibleProtocolVersion {
1116                        client_supported: preferred_versions,
1117                        server_supported: supported,
1118                    });
1119                };
1120                candidate = next;
1121            }
1122            // A correlated JSON-RPC error that is not a modern-era rejection
1123            // and not a version-negotiation signal: the server is legacy.
1124            // The transport delivered a complete response, so a legacy
1125            // `initialize` can follow on the same connection.
1126            Err(error)
1127                if matches!(
1128                    &error,
1129                    ClientInitializeError::JsonRpcError(data)
1130                        if !is_modern_rejection_code(data.code)
1131                ) =>
1132            {
1133                return Ok(DiscoverOutcome::Legacy(Box::new(error)));
1134            }
1135            Err(error) => return Err(error),
1136        }
1137    }
1138}
1139
1140const DISCOVER_CACHE_PREFIX: &str = "server/discover:";
1141const TOOL_LIST_CACHE_PREFIX: &str = "tools/list:";
1142const PROMPT_LIST_CACHE_PREFIX: &str = "prompts/list:";
1143const RESOURCE_LIST_CACHE_PREFIX: &str = "resources/list:";
1144const RESOURCE_TEMPLATE_LIST_CACHE_PREFIX: &str = "resources/templates/list:";
1145const RESOURCE_READ_CACHE_PREFIX: &str = "resources/read:";
1146
1147// Cache keys are built only from the request method plus the parameters that
1148// affect the result (SEP-2549). Request `_meta` (progress tokens, trace
1149// context, etc.) does not affect the result, so it is deliberately excluded to
1150// avoid fragmenting the cache across otherwise-identical requests.
1151fn discover_cache_key() -> String {
1152    // `server/discover` carries no result-affecting parameters.
1153    DISCOVER_CACHE_PREFIX.to_string()
1154}
1155
1156fn list_response_cache_key(prefix: &str, params: &Option<PaginatedRequestParams>) -> String {
1157    // Only the pagination cursor affects which page is returned.
1158    let cursor = params.as_ref().and_then(|params| params.cursor.as_deref());
1159    let cursor =
1160        serde_json::to_string(&cursor).expect("serializing a pagination cursor cannot fail");
1161    format!("{prefix}{cursor}")
1162}
1163
1164fn resource_read_cache_key(params: &ReadResourceRequestParams) -> Option<String> {
1165    // MRTR retries depend on inputs that are not part of the cache key and MUST
1166    // NOT be cached.
1167    if params.input_responses.is_some() || params.request_state.is_some() {
1168        return None;
1169    }
1170    // Only the URI affects the result.
1171    Some(resource_read_cache_prefix_for_uri(&params.uri))
1172}
1173
1174fn resource_read_cache_prefix_for_uri(uri: &str) -> String {
1175    let uri = serde_json::to_string(uri).expect("serializing a resource URI cannot fail");
1176    format!("{RESOURCE_READ_CACHE_PREFIX}{uri}:")
1177}
1178
1179fn request_uses_cursor(params: &Option<PaginatedRequestParams>) -> bool {
1180    params
1181        .as_ref()
1182        .and_then(|params| params.cursor.as_ref())
1183        .is_some()
1184}
1185
1186macro_rules! method {
1187    ($(#[$meta:meta])* peer_req $method:ident $Req:ident() => $Resp: ident ) => {
1188        $(#[$meta])*
1189        pub async fn $method(&self) -> Result<$Resp, ServiceError> {
1190            let result = self
1191                .send_request_cancel_on_drop(ClientRequest::$Req($Req {
1192                    method: Default::default(),
1193                }))
1194                .await?;
1195            match result {
1196                ServerResult::$Resp(result) => Ok(result),
1197                _ => Err(ServiceError::UnexpectedResponse),
1198            }
1199        }
1200    };
1201    ($(#[$meta:meta])* peer_req $method:ident $Req:ident($Param: ident) => $Resp: ident ) => {
1202        $(#[$meta])*
1203        pub async fn $method(&self, params: $Param) -> Result<$Resp, ServiceError> {
1204            let result = self
1205                .send_request_cancel_on_drop(ClientRequest::$Req($Req {
1206                    method: Default::default(),
1207                    params,
1208                    extensions: Default::default(),
1209                }))
1210                .await?;
1211            match result {
1212                ServerResult::$Resp(result) => Ok(result),
1213                _ => Err(ServiceError::UnexpectedResponse),
1214            }
1215        }
1216    };
1217    ($(#[$meta:meta])* peer_req $method:ident $Req:ident($Param: ident)? => $Resp: ident ) => {
1218        $(#[$meta])*
1219        pub async fn $method(&self, params: Option<$Param>) -> Result<$Resp, ServiceError> {
1220            let result = self
1221                .send_request_cancel_on_drop(ClientRequest::$Req($Req {
1222                    method: Default::default(),
1223                    params,
1224                    extensions: Default::default(),
1225                }))
1226                .await?;
1227            match result {
1228                ServerResult::$Resp(result) => Ok(result),
1229                _ => Err(ServiceError::UnexpectedResponse),
1230            }
1231        }
1232    };
1233    ($(#[$meta:meta])* peer_req $method:ident $Req:ident($Param: ident)) => {
1234        $(#[$meta])*
1235        pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> {
1236            let result = self
1237                .send_request_cancel_on_drop(ClientRequest::$Req($Req {
1238                    method: Default::default(),
1239                    params,
1240                    extensions: Default::default(),
1241                }))
1242                .await?;
1243            match result {
1244                ServerResult::EmptyResult(_) => Ok(()),
1245                _ => Err(ServiceError::UnexpectedResponse),
1246            }
1247        }
1248    };
1249
1250    ($(#[$meta:meta])* peer_not $method:ident $Not:ident($Param: ident)) => {
1251        $(#[$meta])*
1252        pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> {
1253            self.send_notification(ClientNotification::$Not($Not {
1254                method: Default::default(),
1255                params,
1256                extensions: Default::default(),
1257            }))
1258            .await?;
1259            Ok(())
1260        }
1261    };
1262    ($(#[$meta:meta])* peer_not $method:ident $Not:ident) => {
1263        $(#[$meta])*
1264        pub async fn $method(&self) -> Result<(), ServiceError> {
1265            self.send_notification(ClientNotification::$Not($Not {
1266                method: Default::default(),
1267                extensions: Default::default(),
1268            }))
1269            .await?;
1270            Ok(())
1271        }
1272    };
1273}
1274
1275impl Peer<RoleClient> {
1276    /// Open a long-lived notification subscription and wait for its acknowledgment.
1277    ///
1278    /// Notifications routed to the returned [`Subscription`] are not also
1279    /// delivered through [`ClientHandler`](crate::ClientHandler) callbacks.
1280    ///
1281    /// # Errors
1282    ///
1283    /// Returns a service, transport, or protocol error when the request cannot
1284    /// be established or the acknowledgment is invalid.
1285    pub async fn listen(
1286        &self,
1287        notifications: SubscriptionFilter,
1288    ) -> Result<Subscription, ServiceError> {
1289        self.listen_with_channel_capacity_inner(
1290            notifications,
1291            DEFAULT_SUBSCRIPTION_CHANNEL_CAPACITY,
1292        )
1293        .await
1294    }
1295
1296    /// Open a subscription with an explicit notification buffer capacity.
1297    ///
1298    /// Notifications routed to the returned [`Subscription`] are not also
1299    /// delivered through [`ClientHandler`](crate::ClientHandler) callbacks.
1300    ///
1301    /// # Errors
1302    ///
1303    /// Returns a service, transport, or protocol error when the request cannot
1304    /// be established or the acknowledgment is invalid.
1305    pub async fn listen_with_capacity(
1306        &self,
1307        notifications: SubscriptionFilter,
1308        channel_capacity: NonZeroUsize,
1309    ) -> Result<Subscription, ServiceError> {
1310        self.listen_with_channel_capacity_inner(notifications, channel_capacity.get())
1311            .await
1312    }
1313
1314    async fn listen_with_channel_capacity_inner(
1315        &self,
1316        notifications: SubscriptionFilter,
1317        channel_capacity: usize,
1318    ) -> Result<Subscription, ServiceError> {
1319        let request = ClientRequest::SubscriptionsListenRequest(SubscriptionsListenRequest::new(
1320            SubscriptionsListenRequestParams::new(notifications.clone()),
1321        ));
1322        let (handle, mut subscription_notifications) = self
1323            .send_subscription_request(request, PeerRequestOptions::no_options(), channel_capacity)
1324            .await?;
1325        let id = handle.id.clone();
1326        let mut pending = PendingSubscriptionRequest::new(handle);
1327
1328        tokio::select! {
1329            biased;
1330            notification = subscription_notifications.recv() => {
1331                let Some(notification) = notification else {
1332                    pending.cancel("subscription stream closed before acknowledgment").await;
1333                    return Err(ServiceError::TransportClosed);
1334                };
1335                if notification.get_meta().subscription_id().as_ref() != Some(&id) {
1336                    pending.cancel("subscription acknowledgment ID mismatch").await;
1337                    return Err(ServiceError::UnexpectedResponse);
1338                }
1339                let ServerNotification::SubscriptionsAcknowledgedNotification(
1340                    acknowledgment,
1341                ) = notification else {
1342                    pending.cancel("notification received before subscription acknowledgment").await;
1343                    return Err(ServiceError::UnexpectedResponse);
1344                };
1345                let accepted = acknowledgment.params.notifications;
1346                if !accepted.is_subset_of(&notifications) {
1347                    pending.cancel("subscription acknowledged an unrequested filter").await;
1348                    return Err(ServiceError::UnexpectedResponse);
1349                }
1350                let Some(handle) = pending.take() else {
1351                    return Err(ServiceError::TransportClosed);
1352                };
1353                Ok(Subscription {
1354                    id,
1355                    acknowledged: accepted,
1356                    notifications: subscription_notifications,
1357                    request: Some(handle),
1358                    end: None,
1359                })
1360            }
1361            response = pending.recv() => {
1362                pending.unregister(&id);
1363                pending.disarm();
1364                let Some(response) = response else {
1365                    return Err(ServiceError::TransportClosed);
1366                };
1367                match response {
1368                    Ok(Err(error)) => Err(error),
1369                    Ok(Ok(_)) => Err(ServiceError::UnexpectedResponse),
1370                    Err(_) => Err(ServiceError::TransportClosed),
1371                }
1372            }
1373        }
1374    }
1375
1376    /// Discover the server's supported protocol versions and capabilities.
1377    ///
1378    /// The high-level client currently exposes this peer only after initialization;
1379    /// pre-initialization probing is planned as follow-up work.
1380    pub async fn discover(&self, meta: RequestMetaObject) -> Result<DiscoverResult, ServiceError> {
1381        let cache_key = discover_cache_key();
1382        if let Some(ServerResult::DiscoverResult(result)) = self.cached_response(&cache_key).await {
1383            return Ok(result);
1384        }
1385        let generation = self.capture_response_cache_generation().await;
1386        let mut request = DiscoverRequest::new(DiscoverRequestParams {});
1387        request.extensions.insert(meta);
1388        let result = self
1389            .send_request(ClientRequest::DiscoverRequest(request))
1390            .await;
1391        let result = match result {
1392            Ok(result) => result,
1393            Err(error) => {
1394                if let Some(ServerResult::DiscoverResult(result)) =
1395                    self.stale_cached_response(&cache_key).await
1396                {
1397                    return Ok(result);
1398                }
1399                return Err(error);
1400            }
1401        };
1402        match result {
1403            ServerResult::DiscoverResult(result) => {
1404                self.cache_result(
1405                    Some(cache_key),
1406                    Some(result.ttl_ms),
1407                    Some(result.cache_scope),
1408                    generation,
1409                    ServerResult::DiscoverResult(result.clone()),
1410                )
1411                .await;
1412                Ok(result)
1413            }
1414            _ => Err(ServiceError::UnexpectedResponse),
1415        }
1416    }
1417
1418    async fn cache_result(
1419        &self,
1420        cache_key: Option<String>,
1421        ttl_ms: Option<u64>,
1422        cache_scope: Option<CacheScope>,
1423        generation: CacheGeneration,
1424        result: ServerResult,
1425    ) {
1426        let Some(cache_key) = cache_key else {
1427            return;
1428        };
1429        self.cache_response_with_generation(cache_key, result, ttl_ms, cache_scope, generation)
1430            .await;
1431    }
1432
1433    pub(crate) async fn invalidate_tool_cache(&self) {
1434        self.invalidate_cached_responses(TOOL_LIST_CACHE_PREFIX)
1435            .await;
1436    }
1437
1438    pub(crate) async fn invalidate_prompt_cache(&self) {
1439        self.invalidate_cached_responses(PROMPT_LIST_CACHE_PREFIX)
1440            .await;
1441    }
1442
1443    pub(crate) async fn invalidate_resource_list_cache(&self) {
1444        self.invalidate_cached_responses(RESOURCE_LIST_CACHE_PREFIX)
1445            .await;
1446        self.invalidate_cached_responses(RESOURCE_TEMPLATE_LIST_CACHE_PREFIX)
1447            .await;
1448    }
1449
1450    pub(crate) async fn invalidate_resource_read_cache(&self, uri: &str) {
1451        self.invalidate_cached_responses(&resource_read_cache_prefix_for_uri(uri))
1452            .await;
1453    }
1454
1455    /// [`Peer::send_request`] that sends `notifications/cancelled` if dropped before
1456    /// the response arrives. Never route `initialize` or `server/discover` through here.
1457    async fn send_request_cancel_on_drop(
1458        &self,
1459        request: ClientRequest,
1460    ) -> Result<ServerResult, ServiceError> {
1461        let handle = self
1462            .send_request_with_option(request, PeerRequestOptions::no_options())
1463            .await?;
1464        let mut guard = AbandonedRequestGuard::new(self, handle.id.clone());
1465        let result = handle.await_response().await;
1466        guard.disarm();
1467        result
1468    }
1469
1470    /// Send one `tools/call` request and return either a final result or an MRTR
1471    /// `InputRequiredResult` without driving any follow-up rounds.
1472    pub async fn call_tool_once(
1473        &self,
1474        params: CallToolRequestParams,
1475    ) -> Result<CallToolResponse, ServiceError> {
1476        let result = self
1477            .send_request_cancel_on_drop(ClientRequest::CallToolRequest(CallToolRequest {
1478                method: Default::default(),
1479                params,
1480                extensions: Default::default(),
1481            }))
1482            .await?;
1483        match result {
1484            ServerResult::CallToolResult(result) => Ok(CallToolResponse::Complete(result)),
1485            ServerResult::InputRequiredResult(result) => {
1486                Ok(CallToolResponse::InputRequired(result))
1487            }
1488            // SEP-2663 Tasks extension: the server materialized a task.
1489            ServerResult::CreateTaskResult(result) => Ok(CallToolResponse::Task(result)),
1490            _ => Err(ServiceError::UnexpectedResponse),
1491        }
1492    }
1493
1494    /// SEP-2663 `tasks/get`: poll the current state of a task.
1495    pub async fn get_task(&self, params: GetTaskParams) -> Result<GetTaskResult, ServiceError> {
1496        let result = self
1497            .send_request_cancel_on_drop(ClientRequest::GetTaskRequest(GetTaskRequest::new(params)))
1498            .await?;
1499        match result {
1500            ServerResult::GetTaskResult(result) => Ok(result),
1501            _ => Err(ServiceError::UnexpectedResponse),
1502        }
1503    }
1504
1505    /// SEP-2663 `tasks/update`: deliver responses to outstanding in-task
1506    /// input requests. The acknowledgement is eventually consistent.
1507    pub async fn update_task(&self, params: UpdateTaskParams) -> Result<(), ServiceError> {
1508        let result = self
1509            .send_request_cancel_on_drop(ClientRequest::UpdateTaskRequest(UpdateTaskRequest::new(
1510                params,
1511            )))
1512            .await?;
1513        match result {
1514            ServerResult::TaskAckResult(_) | ServerResult::EmptyResult(_) => Ok(()),
1515            _ => Err(ServiceError::UnexpectedResponse),
1516        }
1517    }
1518
1519    /// SEP-2663 `tasks/cancel`: signal intent to cancel a task. Cancellation
1520    /// is cooperative; the ack does not guarantee the task stops.
1521    pub async fn cancel_task(&self, params: CancelTaskParams) -> Result<(), ServiceError> {
1522        let result = self
1523            .send_request_cancel_on_drop(ClientRequest::CancelTaskRequest(CancelTaskRequest::new(
1524                params,
1525            )))
1526            .await?;
1527        match result {
1528            ServerResult::TaskAckResult(_) | ServerResult::EmptyResult(_) => Ok(()),
1529            _ => Err(ServiceError::UnexpectedResponse),
1530        }
1531    }
1532
1533    /// Send one `prompts/get` request and return either a final result or an MRTR
1534    /// `InputRequiredResult` without driving any follow-up rounds.
1535    pub async fn get_prompt_once(
1536        &self,
1537        params: GetPromptRequestParams,
1538    ) -> Result<GetPromptResponse, ServiceError> {
1539        let result = self
1540            .send_request_cancel_on_drop(ClientRequest::GetPromptRequest(GetPromptRequest {
1541                method: Default::default(),
1542                params,
1543                extensions: Default::default(),
1544            }))
1545            .await?;
1546        match result {
1547            ServerResult::GetPromptResult(result) => Ok(GetPromptResponse::Complete(result)),
1548            ServerResult::InputRequiredResult(result) => {
1549                Ok(GetPromptResponse::InputRequired(result))
1550            }
1551            _ => Err(ServiceError::UnexpectedResponse),
1552        }
1553    }
1554
1555    /// Send one `resources/read` request and return either a final result or an
1556    /// MRTR `InputRequiredResult` without driving any follow-up rounds.
1557    pub async fn read_resource_once(
1558        &self,
1559        params: ReadResourceRequestParams,
1560    ) -> Result<ReadResourceResponse, ServiceError> {
1561        let cache_key = resource_read_cache_key(&params);
1562        if let Some(key) = cache_key.as_deref()
1563            && let Some(ServerResult::ReadResourceResult(result)) = self.cached_response(key).await
1564        {
1565            return Ok(ReadResourceResponse::Complete(result));
1566        }
1567
1568        let generation = self.capture_response_cache_generation().await;
1569        let result = self
1570            .send_request_cancel_on_drop(ClientRequest::ReadResourceRequest(ReadResourceRequest {
1571                method: Default::default(),
1572                params,
1573                extensions: Default::default(),
1574            }))
1575            .await;
1576        let result = match result {
1577            Ok(result) => result,
1578            Err(error) => {
1579                if let Some(key) = cache_key.as_deref()
1580                    && let Some(ServerResult::ReadResourceResult(result)) =
1581                        self.stale_cached_response(key).await
1582                {
1583                    return Ok(ReadResourceResponse::Complete(result));
1584                }
1585                return Err(error);
1586            }
1587        };
1588        match result {
1589            ServerResult::ReadResourceResult(result) => {
1590                self.cache_result(
1591                    cache_key,
1592                    result.ttl_ms,
1593                    result.cache_scope,
1594                    generation,
1595                    ServerResult::ReadResourceResult(result.clone()),
1596                )
1597                .await;
1598                Ok(ReadResourceResponse::Complete(result))
1599            }
1600            ServerResult::InputRequiredResult(result) => {
1601                Ok(ReadResourceResponse::InputRequired(result))
1602            }
1603            _ => Err(ServiceError::UnexpectedResponse),
1604        }
1605    }
1606
1607    method!(peer_req complete CompleteRequest(CompleteRequestParams) => CompleteResult);
1608    method!(
1609        #[deprecated(
1610            since = "1.8.0",
1611            note = "Logging is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
1612        )]
1613        peer_req set_level SetLevelRequest(SetLevelRequestParams)
1614    );
1615    method!(peer_req get_prompt GetPromptRequest(GetPromptRequestParams) => GetPromptResult);
1616    method!(
1617        #[deprecated(
1618            note = "resources/subscribe is legacy-only; use Peer::listen for protocol version 2026-07-28"
1619        )]
1620        peer_req subscribe SubscribeRequest(SubscribeRequestParams)
1621    );
1622    method!(
1623        #[deprecated(
1624            note = "resources/unsubscribe is legacy-only; cancel the Subscription handle instead"
1625        )]
1626        peer_req unsubscribe UnsubscribeRequest(UnsubscribeRequestParams)
1627    );
1628    method!(peer_req call_tool CallToolRequest(CallToolRequestParams) => CallToolResult);
1629
1630    pub async fn list_prompts(
1631        &self,
1632        params: Option<PaginatedRequestParams>,
1633    ) -> Result<ListPromptsResult, ServiceError> {
1634        let cache_key = list_response_cache_key(PROMPT_LIST_CACHE_PREFIX, &params);
1635        if let Some(ServerResult::ListPromptsResult(result)) =
1636            self.cached_response(&cache_key).await
1637        {
1638            return Ok(result);
1639        }
1640        let generation = self.capture_response_cache_generation().await;
1641        let uses_cursor = request_uses_cursor(&params);
1642        let result = self
1643            .send_request_cancel_on_drop(ClientRequest::ListPromptsRequest(ListPromptsRequest {
1644                method: Default::default(),
1645                params,
1646                extensions: Default::default(),
1647            }))
1648            .await;
1649        let result = match result {
1650            Ok(result) => result,
1651            Err(error) => {
1652                if uses_cursor {
1653                    self.invalidate_prompt_cache().await;
1654                    return Err(error);
1655                }
1656                if let Some(ServerResult::ListPromptsResult(result)) =
1657                    self.stale_cached_response(&cache_key).await
1658                {
1659                    return Ok(result);
1660                }
1661                return Err(error);
1662            }
1663        };
1664        match result {
1665            ServerResult::ListPromptsResult(result) => {
1666                self.cache_result(
1667                    Some(cache_key),
1668                    result.ttl_ms,
1669                    result.cache_scope,
1670                    generation,
1671                    ServerResult::ListPromptsResult(result.clone()),
1672                )
1673                .await;
1674                Ok(result)
1675            }
1676            _ => Err(ServiceError::UnexpectedResponse),
1677        }
1678    }
1679
1680    pub async fn list_resources(
1681        &self,
1682        params: Option<PaginatedRequestParams>,
1683    ) -> Result<ListResourcesResult, ServiceError> {
1684        let cache_key = list_response_cache_key(RESOURCE_LIST_CACHE_PREFIX, &params);
1685        if let Some(ServerResult::ListResourcesResult(result)) =
1686            self.cached_response(&cache_key).await
1687        {
1688            return Ok(result);
1689        }
1690        let generation = self.capture_response_cache_generation().await;
1691        let uses_cursor = request_uses_cursor(&params);
1692        let result = self
1693            .send_request_cancel_on_drop(ClientRequest::ListResourcesRequest(
1694                ListResourcesRequest {
1695                    method: Default::default(),
1696                    params,
1697                    extensions: Default::default(),
1698                },
1699            ))
1700            .await;
1701        let result = match result {
1702            Ok(result) => result,
1703            Err(error) => {
1704                if uses_cursor {
1705                    self.invalidate_cached_responses(RESOURCE_LIST_CACHE_PREFIX)
1706                        .await;
1707                    return Err(error);
1708                }
1709                if let Some(ServerResult::ListResourcesResult(result)) =
1710                    self.stale_cached_response(&cache_key).await
1711                {
1712                    return Ok(result);
1713                }
1714                return Err(error);
1715            }
1716        };
1717        match result {
1718            ServerResult::ListResourcesResult(result) => {
1719                self.cache_result(
1720                    Some(cache_key),
1721                    result.ttl_ms,
1722                    result.cache_scope,
1723                    generation,
1724                    ServerResult::ListResourcesResult(result.clone()),
1725                )
1726                .await;
1727                Ok(result)
1728            }
1729            _ => Err(ServiceError::UnexpectedResponse),
1730        }
1731    }
1732
1733    pub async fn list_resource_templates(
1734        &self,
1735        params: Option<PaginatedRequestParams>,
1736    ) -> Result<ListResourceTemplatesResult, ServiceError> {
1737        let cache_key = list_response_cache_key(RESOURCE_TEMPLATE_LIST_CACHE_PREFIX, &params);
1738        if let Some(ServerResult::ListResourceTemplatesResult(result)) =
1739            self.cached_response(&cache_key).await
1740        {
1741            return Ok(result);
1742        }
1743        let generation = self.capture_response_cache_generation().await;
1744        let uses_cursor = request_uses_cursor(&params);
1745        let result = self
1746            .send_request_cancel_on_drop(ClientRequest::ListResourceTemplatesRequest(
1747                ListResourceTemplatesRequest {
1748                    method: Default::default(),
1749                    params,
1750                    extensions: Default::default(),
1751                },
1752            ))
1753            .await;
1754        let result = match result {
1755            Ok(result) => result,
1756            Err(error) => {
1757                if uses_cursor {
1758                    self.invalidate_cached_responses(RESOURCE_TEMPLATE_LIST_CACHE_PREFIX)
1759                        .await;
1760                    return Err(error);
1761                }
1762                if let Some(ServerResult::ListResourceTemplatesResult(result)) =
1763                    self.stale_cached_response(&cache_key).await
1764                {
1765                    return Ok(result);
1766                }
1767                return Err(error);
1768            }
1769        };
1770        match result {
1771            ServerResult::ListResourceTemplatesResult(result) => {
1772                self.cache_result(
1773                    Some(cache_key),
1774                    result.ttl_ms,
1775                    result.cache_scope,
1776                    generation,
1777                    ServerResult::ListResourceTemplatesResult(result.clone()),
1778                )
1779                .await;
1780                Ok(result)
1781            }
1782            _ => Err(ServiceError::UnexpectedResponse),
1783        }
1784    }
1785
1786    pub async fn read_resource(
1787        &self,
1788        params: ReadResourceRequestParams,
1789    ) -> Result<ReadResourceResult, ServiceError> {
1790        match self.read_resource_once(params).await? {
1791            ReadResourceResponse::Complete(result) => Ok(result),
1792            ReadResourceResponse::InputRequired(_) => Err(ServiceError::UnexpectedResponse),
1793        }
1794    }
1795
1796    pub async fn list_tools(
1797        &self,
1798        params: Option<PaginatedRequestParams>,
1799    ) -> Result<ListToolsResult, ServiceError> {
1800        let cache_key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &params);
1801        if let Some(ServerResult::ListToolsResult(result)) = self.cached_response(&cache_key).await
1802        {
1803            return Ok(result);
1804        }
1805        let generation = self.capture_response_cache_generation().await;
1806        let uses_cursor = request_uses_cursor(&params);
1807        let result = self
1808            .send_request_cancel_on_drop(ClientRequest::ListToolsRequest(ListToolsRequest {
1809                method: Default::default(),
1810                params,
1811                extensions: Default::default(),
1812            }))
1813            .await;
1814        let result = match result {
1815            Ok(result) => result,
1816            Err(error) => {
1817                if uses_cursor {
1818                    self.invalidate_tool_cache().await;
1819                    return Err(error);
1820                }
1821                if let Some(ServerResult::ListToolsResult(result)) =
1822                    self.stale_cached_response(&cache_key).await
1823                {
1824                    return Ok(result);
1825                }
1826                return Err(error);
1827            }
1828        };
1829        match result {
1830            ServerResult::ListToolsResult(result) => {
1831                self.cache_result(
1832                    Some(cache_key),
1833                    result.ttl_ms,
1834                    result.cache_scope,
1835                    generation,
1836                    ServerResult::ListToolsResult(result.clone()),
1837                )
1838                .await;
1839                Ok(result)
1840            }
1841            _ => Err(ServiceError::UnexpectedResponse),
1842        }
1843    }
1844
1845    method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));
1846    method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));
1847    method!(peer_not notify_initialized InitializedNotification);
1848    method!(peer_not notify_roots_list_changed RootsListChangedNotification);
1849}
1850
1851impl Peer<RoleClient> {
1852    /// A wrapper method for [`Peer<RoleClient>::list_tools`].
1853    ///
1854    /// This function will call [`Peer<RoleClient>::list_tools`] multiple times until all tools are listed.
1855    pub async fn list_all_tools(&self) -> Result<Vec<crate::model::Tool>, ServiceError> {
1856        let mut tools = Vec::new();
1857        let mut cursor = None;
1858        loop {
1859            let result = self
1860                .list_tools(Some(PaginatedRequestParams { meta: None, cursor }))
1861                .await?;
1862            tools.extend(result.tools);
1863            cursor = result.next_cursor;
1864            if cursor.is_none() {
1865                break;
1866            }
1867        }
1868        Ok(tools)
1869    }
1870
1871    /// A wrapper method for [`Peer<RoleClient>::list_prompts`].
1872    ///
1873    /// This function will call [`Peer<RoleClient>::list_prompts`] multiple times until all prompts are listed.
1874    pub async fn list_all_prompts(&self) -> Result<Vec<crate::model::Prompt>, ServiceError> {
1875        let mut prompts = Vec::new();
1876        let mut cursor = None;
1877        loop {
1878            let result = self
1879                .list_prompts(Some(PaginatedRequestParams { meta: None, cursor }))
1880                .await?;
1881            prompts.extend(result.prompts);
1882            cursor = result.next_cursor;
1883            if cursor.is_none() {
1884                break;
1885            }
1886        }
1887        Ok(prompts)
1888    }
1889
1890    /// A wrapper method for [`Peer<RoleClient>::list_resources`].
1891    ///
1892    /// This function will call [`Peer<RoleClient>::list_resources`] multiple times until all resources are listed.
1893    pub async fn list_all_resources(&self) -> Result<Vec<crate::model::Resource>, ServiceError> {
1894        let mut resources = Vec::new();
1895        let mut cursor = None;
1896        loop {
1897            let result = self
1898                .list_resources(Some(PaginatedRequestParams { meta: None, cursor }))
1899                .await?;
1900            resources.extend(result.resources);
1901            cursor = result.next_cursor;
1902            if cursor.is_none() {
1903                break;
1904            }
1905        }
1906        Ok(resources)
1907    }
1908
1909    /// A wrapper method for [`Peer<RoleClient>::list_resource_templates`].
1910    ///
1911    /// This function will call [`Peer<RoleClient>::list_resource_templates`] multiple times until all resource templates are listed.
1912    pub async fn list_all_resource_templates(
1913        &self,
1914    ) -> Result<Vec<crate::model::ResourceTemplate>, ServiceError> {
1915        let mut resource_templates = Vec::new();
1916        let mut cursor = None;
1917        loop {
1918            let result = self
1919                .list_resource_templates(Some(PaginatedRequestParams { meta: None, cursor }))
1920                .await?;
1921            resource_templates.extend(result.resource_templates);
1922            cursor = result.next_cursor;
1923            if cursor.is_none() {
1924                break;
1925            }
1926        }
1927        Ok(resource_templates)
1928    }
1929
1930    /// Convenient method to get completion suggestions for a prompt argument
1931    ///
1932    /// # Arguments
1933    /// * `prompt_name` - Name of the prompt being completed
1934    /// * `argument_name` - Name of the argument being completed
1935    /// * `current_value` - Current partial value of the argument
1936    /// * `context` - Optional context with previously resolved arguments
1937    ///
1938    /// # Returns
1939    /// CompletionInfo with suggestions for the specified prompt argument
1940    pub async fn complete_prompt_argument(
1941        &self,
1942        prompt_name: impl Into<String>,
1943        argument_name: impl Into<String>,
1944        current_value: impl Into<String>,
1945        context: Option<CompletionContext>,
1946    ) -> Result<CompletionInfo, ServiceError> {
1947        let request = CompleteRequestParams {
1948            meta: None,
1949            r#ref: Reference::for_prompt(prompt_name),
1950            argument: ArgumentInfo {
1951                name: argument_name.into(),
1952                value: current_value.into(),
1953            },
1954            context,
1955        };
1956
1957        let result = self.complete(request).await?;
1958        Ok(result.completion)
1959    }
1960
1961    /// Convenient method to get completion suggestions for a resource URI argument
1962    ///
1963    /// # Arguments
1964    /// * `uri_template` - URI template pattern being completed
1965    /// * `argument_name` - Name of the URI parameter being completed
1966    /// * `current_value` - Current partial value of the parameter
1967    /// * `context` - Optional context with previously resolved arguments
1968    ///
1969    /// # Returns
1970    /// CompletionInfo with suggestions for the specified resource URI argument
1971    pub async fn complete_resource_argument(
1972        &self,
1973        uri_template: impl Into<String>,
1974        argument_name: impl Into<String>,
1975        current_value: impl Into<String>,
1976        context: Option<CompletionContext>,
1977    ) -> Result<CompletionInfo, ServiceError> {
1978        let request = CompleteRequestParams {
1979            meta: None,
1980            r#ref: Reference::for_resource(uri_template),
1981            argument: ArgumentInfo {
1982                name: argument_name.into(),
1983                value: current_value.into(),
1984            },
1985            context,
1986        };
1987
1988        let result = self.complete(request).await?;
1989        Ok(result.completion)
1990    }
1991
1992    /// Simple completion for a prompt argument without context
1993    ///
1994    /// This is a convenience wrapper around `complete_prompt_argument` for
1995    /// simple completion scenarios that don't require context awareness.
1996    pub async fn complete_prompt_simple(
1997        &self,
1998        prompt_name: impl Into<String>,
1999        argument_name: impl Into<String>,
2000        current_value: impl Into<String>,
2001    ) -> Result<Vec<String>, ServiceError> {
2002        let completion = self
2003            .complete_prompt_argument(prompt_name, argument_name, current_value, None)
2004            .await?;
2005        Ok(completion.values)
2006    }
2007
2008    /// Simple completion for a resource URI argument without context
2009    ///
2010    /// This is a convenience wrapper around `complete_resource_argument` for
2011    /// simple completion scenarios that don't require context awareness.
2012    pub async fn complete_resource_simple(
2013        &self,
2014        uri_template: impl Into<String>,
2015        argument_name: impl Into<String>,
2016        current_value: impl Into<String>,
2017    ) -> Result<Vec<String>, ServiceError> {
2018        let completion = self
2019            .complete_resource_argument(uri_template, argument_name, current_value, None)
2020            .await?;
2021        Ok(completion.values)
2022    }
2023}
2024
2025impl<S> RunningService<RoleClient, S>
2026where
2027    S: Service<RoleClient>,
2028{
2029    /// Send one `tools/call` request without driving MRTR follow-up rounds.
2030    pub async fn call_tool_once(
2031        &self,
2032        params: CallToolRequestParams,
2033    ) -> Result<CallToolResponse, ServiceError> {
2034        self.peer.call_tool_once(params).await
2035    }
2036
2037    /// Send one `prompts/get` request without driving MRTR follow-up rounds.
2038    pub async fn get_prompt_once(
2039        &self,
2040        params: GetPromptRequestParams,
2041    ) -> Result<GetPromptResponse, ServiceError> {
2042        self.peer.get_prompt_once(params).await
2043    }
2044
2045    /// Send one `resources/read` request without driving MRTR follow-up rounds.
2046    pub async fn read_resource_once(
2047        &self,
2048        params: ReadResourceRequestParams,
2049    ) -> Result<ReadResourceResponse, ServiceError> {
2050        self.peer.read_resource_once(params).await
2051    }
2052
2053    /// High-level `tools/call` helper that automatically fulfils SEP-2322
2054    /// `input_required` rounds through the local [`ClientHandler`](crate::ClientHandler) service.
2055    ///
2056    /// # Errors
2057    ///
2058    /// Returns [`ServiceError::InputRequiredRoundsExceeded`] if the peer does
2059    /// not produce a final [`CallToolResult`] within the default MRTR round cap.
2060    /// Other transport, protocol, and local input-handler errors are propagated.
2061    pub async fn call_tool(
2062        &self,
2063        params: CallToolRequestParams,
2064    ) -> Result<CallToolResult, ServiceError> {
2065        self.call_tool_with_mrtr_max_rounds(params, DEFAULT_MRTR_MAX_ROUNDS)
2066            .await
2067    }
2068
2069    /// Same as [`Self::call_tool`], with an explicit MRTR round cap.
2070    ///
2071    /// # Errors
2072    ///
2073    /// Returns [`ServiceError::InputRequiredRoundsExceeded`] once `max_rounds`
2074    /// `input_required` responses have been driven without receiving a final
2075    /// [`CallToolResult`]. Other transport, protocol, and local input-handler
2076    /// errors are propagated.
2077    pub async fn call_tool_with_mrtr_max_rounds(
2078        &self,
2079        mut params: CallToolRequestParams,
2080        max_rounds: usize,
2081    ) -> Result<CallToolResult, ServiceError> {
2082        let mut state_only_rounds = 0usize;
2083        for _round in 0..max_rounds {
2084            match self.peer.call_tool_once(params.clone()).await? {
2085                CallToolResponse::Complete(result) => return Ok(result),
2086                CallToolResponse::InputRequired(result) => {
2087                    let (input_responses, request_state) = self
2088                        .prepare_input_required_retry(result, &mut state_only_rounds)
2089                        .await?;
2090                    params.input_responses = input_responses;
2091                    params.request_state = request_state;
2092                }
2093                // SEP-2663: this helper does not drive the task polling
2094                // lifecycle. Callers that declare the tasks extension
2095                // capability should use `call_tool_once` and poll `tasks/get`.
2096                CallToolResponse::Task(_) => return Err(ServiceError::UnexpectedResponse),
2097            }
2098        }
2099        Err(ServiceError::InputRequiredRoundsExceeded { max_rounds })
2100    }
2101
2102    /// High-level `prompts/get` helper that automatically fulfils SEP-2322
2103    /// `input_required` rounds through the local [`ClientHandler`](crate::ClientHandler) service.
2104    ///
2105    /// # Errors
2106    ///
2107    /// Returns [`ServiceError::InputRequiredRoundsExceeded`] if the peer does
2108    /// not produce a final [`GetPromptResult`] within the default MRTR round cap.
2109    /// Other transport, protocol, and local input-handler errors are propagated.
2110    pub async fn get_prompt(
2111        &self,
2112        params: GetPromptRequestParams,
2113    ) -> Result<GetPromptResult, ServiceError> {
2114        self.get_prompt_with_mrtr_max_rounds(params, DEFAULT_MRTR_MAX_ROUNDS)
2115            .await
2116    }
2117
2118    /// Same as [`Self::get_prompt`], with an explicit MRTR round cap.
2119    ///
2120    /// # Errors
2121    ///
2122    /// Returns [`ServiceError::InputRequiredRoundsExceeded`] once `max_rounds`
2123    /// `input_required` responses have been driven without receiving a final
2124    /// [`GetPromptResult`]. Other transport, protocol, and local input-handler
2125    /// errors are propagated.
2126    pub async fn get_prompt_with_mrtr_max_rounds(
2127        &self,
2128        mut params: GetPromptRequestParams,
2129        max_rounds: usize,
2130    ) -> Result<GetPromptResult, ServiceError> {
2131        let mut state_only_rounds = 0usize;
2132        for _round in 0..max_rounds {
2133            match self.peer.get_prompt_once(params.clone()).await? {
2134                GetPromptResponse::Complete(result) => return Ok(result),
2135                GetPromptResponse::InputRequired(result) => {
2136                    let (input_responses, request_state) = self
2137                        .prepare_input_required_retry(result, &mut state_only_rounds)
2138                        .await?;
2139                    params.input_responses = input_responses;
2140                    params.request_state = request_state;
2141                }
2142            }
2143        }
2144        Err(ServiceError::InputRequiredRoundsExceeded { max_rounds })
2145    }
2146
2147    /// High-level `resources/read` helper that automatically fulfils SEP-2322
2148    /// `input_required` rounds through the local [`ClientHandler`](crate::ClientHandler) service.
2149    ///
2150    /// # Errors
2151    ///
2152    /// Returns [`ServiceError::InputRequiredRoundsExceeded`] if the peer does
2153    /// not produce a final [`ReadResourceResult`] within the default MRTR round
2154    /// cap. Other transport, protocol, and local input-handler errors are
2155    /// propagated.
2156    pub async fn read_resource(
2157        &self,
2158        params: ReadResourceRequestParams,
2159    ) -> Result<ReadResourceResult, ServiceError> {
2160        self.read_resource_with_mrtr_max_rounds(params, DEFAULT_MRTR_MAX_ROUNDS)
2161            .await
2162    }
2163
2164    /// Same as [`Self::read_resource`], with an explicit MRTR round cap.
2165    ///
2166    /// # Errors
2167    ///
2168    /// Returns [`ServiceError::InputRequiredRoundsExceeded`] once `max_rounds`
2169    /// `input_required` responses have been driven without receiving a final
2170    /// [`ReadResourceResult`]. Other transport, protocol, and local input-handler
2171    /// errors are propagated.
2172    pub async fn read_resource_with_mrtr_max_rounds(
2173        &self,
2174        mut params: ReadResourceRequestParams,
2175        max_rounds: usize,
2176    ) -> Result<ReadResourceResult, ServiceError> {
2177        let mut state_only_rounds = 0usize;
2178        for _round in 0..max_rounds {
2179            match self.peer.read_resource_once(params.clone()).await? {
2180                ReadResourceResponse::Complete(result) => return Ok(result),
2181                ReadResourceResponse::InputRequired(result) => {
2182                    let (input_responses, request_state) = self
2183                        .prepare_input_required_retry(result, &mut state_only_rounds)
2184                        .await?;
2185                    params.input_responses = input_responses;
2186                    params.request_state = request_state;
2187                }
2188            }
2189        }
2190        Err(ServiceError::InputRequiredRoundsExceeded { max_rounds })
2191    }
2192
2193    async fn prepare_input_required_retry(
2194        &self,
2195        result: InputRequiredResult,
2196        state_only_rounds: &mut usize,
2197    ) -> Result<(Option<InputResponses>, Option<String>), ServiceError> {
2198        let had_input_requests = result
2199            .input_requests
2200            .as_ref()
2201            .is_some_and(|requests| !requests.is_empty());
2202        if !had_input_requests && result.request_state.is_none() {
2203            return Err(ServiceError::UnexpectedResponse);
2204        }
2205
2206        let responses = self
2207            .fulfill_input_requests(result.input_requests.unwrap_or_default())
2208            .await?;
2209        if had_input_requests {
2210            *state_only_rounds = 0;
2211        } else {
2212            Self::sleep_state_only_round(*state_only_rounds).await;
2213            *state_only_rounds += 1;
2214        }
2215
2216        Ok((
2217            (!responses.is_empty()).then_some(responses),
2218            result.request_state,
2219        ))
2220    }
2221
2222    async fn fulfill_input_requests(
2223        &self,
2224        requests: crate::model::InputRequests,
2225    ) -> Result<InputResponses, ServiceError> {
2226        let responses = futures::future::try_join_all(
2227            requests
2228                .into_iter()
2229                .map(|(key, request)| self.fulfill_input_request(key, request)),
2230        )
2231        .await?;
2232        Ok(responses.into_iter().collect())
2233    }
2234
2235    async fn fulfill_input_request(
2236        &self,
2237        key: String,
2238        request: InputRequest,
2239    ) -> Result<(String, serde_json::Value), ServiceError> {
2240        let response = match request {
2241            InputRequest::CreateMessage(request) => {
2242                let mut request = ServerRequest::CreateMessageRequest(request);
2243                let context = self.input_request_context(&key, &mut request);
2244                match self
2245                    .service
2246                    .handle_request(request, context)
2247                    .await
2248                    .map_err(ServiceError::McpError)?
2249                {
2250                    ClientResult::CreateMessageResult(result) => {
2251                        serde_json::to_value(result).map_err(Self::serde_to_service_error)?
2252                    }
2253                    _ => return Err(ServiceError::UnexpectedResponse),
2254                }
2255            }
2256            InputRequest::Elicitation(request) => {
2257                let mut request = ServerRequest::ElicitRequest(request);
2258                let context = self.input_request_context(&key, &mut request);
2259                match self
2260                    .service
2261                    .handle_request(request, context)
2262                    .await
2263                    .map_err(ServiceError::McpError)?
2264                {
2265                    ClientResult::ElicitResult(result) => {
2266                        serde_json::to_value(result).map_err(Self::serde_to_service_error)?
2267                    }
2268                    _ => return Err(ServiceError::UnexpectedResponse),
2269                }
2270            }
2271            InputRequest::ListRoots(request) => {
2272                let mut request = ServerRequest::ListRootsRequest(request);
2273                let context = self.input_request_context(&key, &mut request);
2274                match self
2275                    .service
2276                    .handle_request(request, context)
2277                    .await
2278                    .map_err(ServiceError::McpError)?
2279                {
2280                    ClientResult::ListRootsResult(result) => {
2281                        serde_json::to_value(result).map_err(Self::serde_to_service_error)?
2282                    }
2283                    _ => return Err(ServiceError::UnexpectedResponse),
2284                }
2285            }
2286        };
2287        Ok((key, response))
2288    }
2289
2290    fn input_request_context<T>(&self, key: &str, request: &mut T) -> RequestContext<RoleClient>
2291    where
2292        T: GetMeta<Metadata = crate::model::RequestMetaObject> + GetExtensions,
2293    {
2294        let mut meta = Default::default();
2295        let mut extensions = Default::default();
2296        std::mem::swap(&mut meta, request.get_meta_mut());
2297        std::mem::swap(&mut extensions, request.extensions_mut());
2298        RequestContext {
2299            ct: tokio_util::sync::CancellationToken::new(),
2300            id: NumberOrString::String(Arc::from(key)),
2301            peer: self.peer.clone(),
2302            meta,
2303            extensions,
2304        }
2305    }
2306
2307    async fn sleep_state_only_round(state_only_rounds: usize) {
2308        let millis = (50u64.saturating_mul(1_u64 << state_only_rounds.min(3))).min(250);
2309        tokio::time::sleep(Duration::from_millis(millis)).await;
2310    }
2311
2312    fn serde_to_service_error(error: serde_json::Error) -> ServiceError {
2313        ServiceError::McpError(ErrorData::internal_error(
2314            format!("failed to serialize MRTR input response: {error}"),
2315            None,
2316        ))
2317    }
2318}
2319
2320#[cfg(test)]
2321mod tests {
2322    use super::*;
2323
2324    #[tokio::test]
2325    async fn server_cancellation_retires_subscription_responder() {
2326        tokio::task::LocalSet::new()
2327            .run_until(async {
2328                for (discover, version) in [
2329                    (false, ProtocolVersion::V_2026_07_28),
2330                    (true, ProtocolVersion::V_2026_07_28),
2331                    (true, ProtocolVersion::V_2025_11_25),
2332                ] {
2333                    tokio::time::timeout(
2334                        Duration::from_secs(5),
2335                        check_subscription_cancellation(discover, version),
2336                    )
2337                    .await
2338                    .expect("subscription cancellation timed out");
2339                }
2340            })
2341            .await;
2342    }
2343
2344    async fn check_subscription_cancellation(discover: bool, version: ProtocolVersion) {
2345        use crate::model::{
2346            GetMeta, PingRequest, ServerInfo, SubscriptionsAcknowledgedNotification,
2347            SubscriptionsAcknowledgedNotificationParams,
2348        };
2349
2350        let (server_transport, client_transport) = tokio::io::duplex(4096);
2351        let mut server =
2352            crate::transport::IntoTransport::<RoleServer, _, _>::into_transport(server_transport);
2353        let info = ServerInfo {
2354            protocol_version: version.clone(),
2355            ..Default::default()
2356        };
2357        let client = if discover {
2358            let (client, ()) = tokio::join!(
2359                serve_client_with_lifecycle(
2360                    (),
2361                    client_transport,
2362                    ClientLifecycleMode::Discover {
2363                        preferred_versions: vec![version.clone()]
2364                    }
2365                ),
2366                async {
2367                    let Some(ClientJsonRpcMessage::Request(request)) = server.receive().await
2368                    else {
2369                        panic!("expected discover request");
2370                    };
2371                    assert!(matches!(request.request, ClientRequest::DiscoverRequest(_)));
2372                    server
2373                        .send(ServerJsonRpcMessage::response(
2374                            ServerResult::DiscoverResult(DiscoverResult::new(
2375                                vec![version],
2376                                info.capabilities,
2377                            )),
2378                            request.id,
2379                        ))
2380                        .await
2381                        .unwrap();
2382                }
2383            );
2384            client.unwrap()
2385        } else {
2386            super::super::serve_directly::<RoleClient, _, _, _, _>(
2387                (),
2388                client_transport,
2389                Some(info.into()),
2390            )
2391        };
2392        let filter = SubscriptionFilter::default();
2393        let (subscription, ()) = tokio::join!(client.listen(filter.clone()), async {
2394            let Some(ClientJsonRpcMessage::Request(request)) = server.receive().await else {
2395                panic!("expected listen request");
2396            };
2397            assert!(matches!(
2398                request.request,
2399                ClientRequest::SubscriptionsListenRequest(_)
2400            ));
2401            let mut ack: ServerNotification = SubscriptionsAcknowledgedNotification::new(
2402                SubscriptionsAcknowledgedNotificationParams::new(filter),
2403            )
2404            .into();
2405            ack.get_meta_mut().set_subscription_id(request.id);
2406            server
2407                .send(ServerJsonRpcMessage::notification(ack))
2408                .await
2409                .unwrap();
2410        });
2411        let mut subscription = subscription.unwrap();
2412        let id = subscription.id().clone();
2413        let mut ordinary = client
2414            .send_cancellable_request(
2415                PingRequest {
2416                    method: Default::default(),
2417                    extensions: Default::default(),
2418                }
2419                .into(),
2420                PeerRequestOptions::no_options(),
2421            )
2422            .await
2423            .unwrap();
2424        assert!(matches!(
2425            server.receive().await,
2426            Some(ClientJsonRpcMessage::Request(_))
2427        ));
2428        for unknown in [
2429            ordinary.id.clone(),
2430            RequestId::String(id.to_string().into()),
2431            RequestId::String("unknown".into()),
2432        ] {
2433            server
2434                .send(ServerJsonRpcMessage::notification(
2435                    CancelledNotification::new(CancelledNotificationParam::new(
2436                        Some(unknown),
2437                        None,
2438                    ))
2439                    .into(),
2440                ))
2441                .await
2442                .unwrap();
2443        }
2444        server
2445            .send(ServerJsonRpcMessage::request(
2446                PingRequest {
2447                    method: Default::default(),
2448                    extensions: Default::default(),
2449                }
2450                .into(),
2451                RequestId::String("before-cancellation".into()),
2452            ))
2453            .await
2454            .unwrap();
2455        assert!(matches!(
2456            server.receive().await,
2457            Some(ClientJsonRpcMessage::Response(_))
2458        ));
2459        assert!(matches!(
2460            subscription.request.as_mut().unwrap().rx.try_recv(),
2461            Err(tokio::sync::oneshot::error::TryRecvError::Empty)
2462        ));
2463        assert!(matches!(
2464            ordinary.rx.try_recv(),
2465            Err(tokio::sync::oneshot::error::TryRecvError::Empty)
2466        ));
2467        assert!(matches!(
2468            subscription.notifications.try_recv(),
2469            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2470        ));
2471        server
2472            .send(ServerJsonRpcMessage::notification(
2473                CancelledNotification::new(CancelledNotificationParam::new(
2474                    Some(id.clone()),
2475                    Some("subscription ended".to_owned()),
2476                ))
2477                .into(),
2478            ))
2479            .await
2480            .unwrap();
2481        // Ordered input makes the ping response a barrier for cancellation handling.
2482        server
2483            .send(ServerJsonRpcMessage::request(
2484                PingRequest {
2485                    method: Default::default(),
2486                    extensions: Default::default(),
2487                }
2488                .into(),
2489                RequestId::String("after-cancellation".into()),
2490            ))
2491            .await
2492            .unwrap();
2493        assert!(matches!(
2494            server.receive().await,
2495            Some(ClientJsonRpcMessage::Response(_))
2496        ));
2497        assert!(
2498            matches!(
2499                subscription.request.as_mut().unwrap().rx.try_recv(),
2500                Ok(Err(ServiceError::Cancelled { .. }))
2501            ),
2502            "the outbound responder must retire without a final response or disconnect"
2503        );
2504        assert!(subscription.next().await.unwrap().is_none());
2505        assert!(matches!(
2506            subscription.end(),
2507            Some(SubscriptionEnd::Cancelled)
2508        ));
2509        assert!(client.peer().subscription_sender(&id).is_none());
2510        server
2511            .send(ServerJsonRpcMessage::response(
2512                ServerResult::empty(()),
2513                ordinary.id.clone(),
2514            ))
2515            .await
2516            .unwrap();
2517        assert!(ordinary.await_response().await.is_ok());
2518
2519        // Raw listen requests also retire, even without a registered notification channel.
2520        let raw = client
2521            .send_cancellable_request(
2522                ClientRequest::SubscriptionsListenRequest(SubscriptionsListenRequest::new(
2523                    SubscriptionsListenRequestParams::new(SubscriptionFilter::default()),
2524                )),
2525                PeerRequestOptions::no_options(),
2526            )
2527            .await
2528            .unwrap();
2529        assert!(matches!(
2530            server.receive().await,
2531            Some(ClientJsonRpcMessage::Request(_))
2532        ));
2533        server
2534            .send(ServerJsonRpcMessage::notification(
2535                CancelledNotification::new(CancelledNotificationParam::new(
2536                    Some(raw.id.clone()),
2537                    None,
2538                ))
2539                .into(),
2540            ))
2541            .await
2542            .unwrap();
2543        assert!(matches!(
2544            raw.await_response().await,
2545            Err(ServiceError::Cancelled { .. })
2546        ));
2547        client.cancel().await.unwrap();
2548    }
2549
2550    #[tokio::test]
2551    async fn auto_startup_falls_back_when_discover_is_ignored() {
2552        use crate::model::{InitializeResult, ServerCapabilities};
2553
2554        tokio::task::LocalSet::new()
2555            .run_until(async {
2556                let (server_transport, client_transport) = tokio::io::duplex(4096);
2557                let mut server =
2558                    crate::transport::IntoTransport::<RoleServer, _, _>::into_transport(
2559                        server_transport,
2560                    );
2561                let server_task = tokio::task::spawn_local(async move {
2562                    let ClientJsonRpcMessage::Request(discover) =
2563                        server.receive().await.expect("expected discover request")
2564                    else {
2565                        panic!("expected discover request");
2566                    };
2567                    assert!(matches!(
2568                        discover.request,
2569                        ClientRequest::DiscoverRequest(_)
2570                    ));
2571
2572                    let ClientJsonRpcMessage::Request(initialize) =
2573                        server.receive().await.expect("expected initialize request")
2574                    else {
2575                        panic!("expected initialize request");
2576                    };
2577                    assert!(matches!(
2578                        initialize.request,
2579                        ClientRequest::InitializeRequest(_)
2580                    ));
2581                    server
2582                        .send(ServerJsonRpcMessage::response(
2583                            ServerResult::InitializeResult(InitializeResult::new(
2584                                ServerCapabilities::default(),
2585                            )),
2586                            initialize.id,
2587                        ))
2588                        .await
2589                        .expect("send initialize response");
2590                    assert!(matches!(
2591                        server.receive().await,
2592                        Some(ClientJsonRpcMessage::Notification(_))
2593                    ));
2594                });
2595
2596                let client_transport =
2597                    crate::transport::IntoTransport::<RoleClient, _, _>::into_transport(
2598                        client_transport,
2599                    );
2600                let client = serve_client_with_ct_inner(
2601                    (),
2602                    client_transport,
2603                    ClientLifecycleMode::Auto {
2604                        preferred_versions: vec![ProtocolVersion::V_2026_07_28],
2605                        legacy_version: Some(ProtocolVersion::V_2025_11_25),
2606                    },
2607                    CancellationToken::new(),
2608                    Duration::from_millis(25),
2609                )
2610                .await
2611                .expect("auto client should fall back after discover timeout");
2612                client.cancel().await.expect("cancel client");
2613                server_task.await.expect("server task");
2614            })
2615            .await;
2616    }
2617
2618    fn disconnected_peer() -> Peer<RoleClient> {
2619        let (peer, receiver) =
2620            Peer::<RoleClient>::new(Arc::new(AtomicU32RequestIdProvider::default()), None);
2621        drop(receiver);
2622        peer
2623    }
2624
2625    fn tools_result(ttl_ms: Option<u64>, cache_scope: Option<CacheScope>) -> ListToolsResult {
2626        let mut result = ListToolsResult::with_all_items(Vec::new());
2627        result.ttl_ms = ttl_ms;
2628        result.cache_scope = cache_scope;
2629        result
2630    }
2631
2632    #[tokio::test]
2633    async fn fresh_cached_page_is_served_without_transport_io() {
2634        let peer = disconnected_peer();
2635        let params = None::<PaginatedRequestParams>;
2636        let key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &params);
2637        let expected = tools_result(Some(5_000), Some(CacheScope::Public));
2638        peer.cache_response(
2639            key,
2640            ServerResult::ListToolsResult(expected.clone()),
2641            expected.ttl_ms,
2642            expected.cache_scope,
2643        )
2644        .await;
2645
2646        assert_eq!(peer.list_tools(params).await.unwrap(), expected);
2647    }
2648
2649    #[tokio::test]
2650    async fn expired_entry_falls_through_to_the_transport() {
2651        let peer = disconnected_peer();
2652        peer.set_response_cache_config(
2653            ClientCacheConfig::default().with_serve_stale_on_error(false),
2654        )
2655        .await;
2656        let params = None::<PaginatedRequestParams>;
2657        let key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &params);
2658        peer.cache_response(
2659            key,
2660            ServerResult::ListToolsResult(tools_result(Some(1), Some(CacheScope::Public))),
2661            Some(1),
2662            Some(CacheScope::Public),
2663        )
2664        .await;
2665        tokio::time::sleep(Duration::from_millis(5)).await;
2666
2667        assert!(matches!(
2668            peer.list_tools(params).await,
2669            Err(ServiceError::TransportClosed)
2670        ));
2671    }
2672
2673    #[tokio::test]
2674    async fn private_entries_are_isolated_between_authorization_partitions() {
2675        let peer = disconnected_peer();
2676        let key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &None);
2677
2678        peer.set_response_cache_config(
2679            ClientCacheConfig::default().with_private_partition("auth-a"),
2680        )
2681        .await;
2682        peer.cache_response(
2683            key.clone(),
2684            ServerResult::ListToolsResult(tools_result(Some(5_000), Some(CacheScope::Private))),
2685            Some(5_000),
2686            Some(CacheScope::Private),
2687        )
2688        .await;
2689        assert!(peer.cached_response(&key).await.is_some());
2690
2691        // Switching to a different authorization context must not expose the
2692        // first partition's private entry.
2693        peer.set_response_cache_config(
2694            ClientCacheConfig::default().with_private_partition("auth-b"),
2695        )
2696        .await;
2697        assert!(peer.cached_response(&key).await.is_none());
2698    }
2699
2700    #[tokio::test]
2701    async fn list_change_notification_discards_every_cached_page() {
2702        let peer = disconnected_peer();
2703        for cursor in [None, Some("page-a".into()), Some("page-b".into())] {
2704            let params =
2705                cursor.map(|cursor| PaginatedRequestParams::default().with_cursor(Some(cursor)));
2706            let key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &params);
2707            peer.cache_response(
2708                key,
2709                ServerResult::ListToolsResult(tools_result(Some(5_000), Some(CacheScope::Public))),
2710                Some(5_000),
2711                Some(CacheScope::Public),
2712            )
2713            .await;
2714        }
2715
2716        peer.invalidate_tool_cache().await;
2717
2718        for cursor in [None, Some("page-a".into()), Some("page-b".into())] {
2719            let params =
2720                cursor.map(|cursor| PaginatedRequestParams::default().with_cursor(Some(cursor)));
2721            let key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &params);
2722            assert!(peer.cached_response(&key).await.is_none());
2723        }
2724    }
2725
2726    #[tokio::test]
2727    async fn expired_entry_is_served_when_refetch_fails() {
2728        let peer = disconnected_peer();
2729        let params = None::<PaginatedRequestParams>;
2730        let key = list_response_cache_key(TOOL_LIST_CACHE_PREFIX, &params);
2731        let expected = tools_result(Some(1), Some(CacheScope::Public));
2732        peer.cache_response(
2733            key,
2734            ServerResult::ListToolsResult(expected.clone()),
2735            Some(1),
2736            Some(CacheScope::Public),
2737        )
2738        .await;
2739        tokio::time::sleep(Duration::from_millis(5)).await;
2740
2741        assert_eq!(peer.list_tools(params).await.unwrap(), expected);
2742    }
2743
2744    #[tokio::test]
2745    async fn discover_serves_a_fresh_cached_response_without_transport_io() {
2746        let peer = disconnected_peer();
2747        let meta = RequestMetaObject::default();
2748        let key = discover_cache_key();
2749        let expected = DiscoverResult::new(vec![ProtocolVersion::default()], Default::default())
2750            .with_server_info(crate::model::Implementation::from_build_env())
2751            .with_ttl_ms(5_000)
2752            .with_cache_scope(CacheScope::Public);
2753        peer.cache_response(
2754            key,
2755            ServerResult::DiscoverResult(expected.clone()),
2756            Some(5_000),
2757            Some(CacheScope::Public),
2758        )
2759        .await;
2760
2761        assert_eq!(peer.discover(meta).await.unwrap(), expected);
2762    }
2763}
2764
2765#[cfg(test)]
2766mod sep2260_association_tests {
2767    use super::*;
2768    use crate::{
2769        model::{
2770            CreateMessageRequest, CreateMessageRequestParams, SamplingMessage, ServerCapabilities,
2771        },
2772        service::PeerRequestAssociation,
2773    };
2774
2775    fn sampling_request() -> ServerRequest {
2776        ServerRequest::CreateMessageRequest(CreateMessageRequest::new(
2777            CreateMessageRequestParams::new(vec![SamplingMessage::user_text("hi")], 16),
2778        ))
2779    }
2780
2781    fn server_info(version: ProtocolVersion) -> ServerPeerInfo {
2782        ServerPeerInfo::new(version, ServerCapabilities::default())
2783    }
2784
2785    fn enforce(
2786        info: &ServerPeerInfo,
2787        association: PeerRequestAssociation,
2788    ) -> Result<(), ErrorData> {
2789        RoleClient::enforce_peer_request_association(&sampling_request(), Some(info), association)
2790    }
2791
2792    #[test]
2793    fn strict_rejects_unassociated() {
2794        let info = server_info(ProtocolVersion::V_2026_07_28);
2795        let err = enforce(&info, PeerRequestAssociation::Unassociated).unwrap_err();
2796        assert_eq!(err.code, crate::model::ErrorCode::INVALID_PARAMS);
2797    }
2798
2799    #[test]
2800    fn strict_accepts_associated() {
2801        let info = server_info(ProtocolVersion::V_2026_07_28);
2802        assert!(enforce(&info, PeerRequestAssociation::Associated).is_ok());
2803    }
2804
2805    #[test]
2806    fn strict_unknown_falls_back_to_coarse_check() {
2807        let info = server_info(ProtocolVersion::V_2026_07_28);
2808        assert!(
2809            enforce(
2810                &info,
2811                PeerRequestAssociation::Unknown {
2812                    has_pending_outbound_request: true
2813                }
2814            )
2815            .is_ok()
2816        );
2817        assert!(
2818            enforce(
2819                &info,
2820                PeerRequestAssociation::Unknown {
2821                    has_pending_outbound_request: false
2822                }
2823            )
2824            .is_err()
2825        );
2826    }
2827
2828    #[test]
2829    fn legacy_protocol_accepts_even_unassociated() {
2830        let info = server_info(ProtocolVersion::V_2025_11_25);
2831        assert!(enforce(&info, PeerRequestAssociation::Unassociated).is_ok());
2832    }
2833}