Skip to main content

tower_mcp/client/
mod.rs

1//! MCP Client with bidirectional communication support.
2//!
3//! Provides [`McpClient`] for connecting to MCP servers over any
4//! [`ClientTransport`]. The client runs a background message loop that
5//! handles request/response correlation, server-initiated requests
6//! (sampling, elicitation, roots), and notifications.
7//!
8//! See [`crate::guides::client`] for transport selection, lifecycle setup,
9//! callbacks, common requests, caching, retry policy, and shutdown guidance.
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! use tower_mcp::client::{McpClient, StdioClientTransport};
15//!
16//! #[tokio::main]
17//! async fn main() -> Result<(), tower_mcp::BoxError> {
18//!     let transport = StdioClientTransport::spawn("my-mcp-server", &["--flag"]).await?;
19//!     let client = McpClient::connect(transport).await?;
20//!
21//!     let server_info = client.initialize("my-client", "1.0.0").await?;
22//!     println!("Connected to: {}", server_info.server_info.name);
23//!
24//!     let tools = client.list_tools().await?;
25//!     for tool in &tools.tools {
26//!         println!("Tool: {}", tool.name);
27//!     }
28//!
29//!     let result = client.call_tool("my-tool", serde_json::json!({"arg": "value"})).await?;
30//!     println!("Result: {:?}", result);
31//!
32//!     Ok(())
33//! }
34//! ```
35
36mod channel;
37mod handler;
38#[cfg(feature = "http-client")]
39mod http;
40#[cfg(feature = "oauth-client")]
41mod oauth;
42#[cfg(feature = "oauth-client")]
43mod oauth_authcode;
44#[cfg(feature = "oauth-client")]
45mod oauth_flow;
46mod response_cache;
47mod stdio;
48mod transport;
49#[cfg(feature = "websocket")]
50mod websocket;
51
52pub use channel::ChannelTransport;
53pub use handler::{ClientHandler, NotificationHandler, ServerNotification};
54#[cfg(feature = "http-client")]
55pub use http::{HttpClientConfig, HttpClientTransport};
56#[cfg(feature = "oauth-client")]
57pub use oauth::{
58    OAuthBearerChallenge, OAuthClientCredentials, OAuthClientCredentialsBuilder, OAuthClientError,
59    OAuthScopeChallenge, OAuthScopeEscalationConfig, OAuthScopeEscalationHandler,
60    OAuthScopeEscalationRequest, OAuthTokenEndpointAuthMethod, TokenProvider,
61};
62#[cfg(feature = "oauth-client")]
63pub use oauth_authcode::{
64    MemoryOAuthClientRegistrationStore, OAuthApplicationType, OAuthAuthCodeConfig,
65    OAuthAuthorizationCode, OAuthAuthorizationDiscovery, OAuthAuthorizationServerMetadata,
66    OAuthClientRegistration, OAuthClientRegistrationMethod, OAuthClientRegistrationOptions,
67    OAuthClientRegistrationStore, OAuthDynamicClientRegistration, OAuthProtectedResourceMetadata,
68    discover_oauth_authorization, discover_oauth_authorization_server,
69    probe_oauth_bearer_challenge, resolve_oauth_client_registration,
70    resolve_oauth_client_registration_with_store,
71};
72#[cfg(feature = "oauth-client")]
73pub use oauth_flow::{
74    MemoryOAuthAuthorizationStateStore, MemoryOAuthTokenStore, OAuthAuthorizationAction,
75    OAuthAuthorizationFlow, OAuthAuthorizationFlowBuilder, OAuthAuthorizationHandler,
76    OAuthAuthorizationRequest, OAuthAuthorizationStart, OAuthAuthorizationStateStore,
77    OAuthClientAssertionRequest, OAuthClientAssertionSigner, OAuthHttpBody, OAuthHttpClient,
78    OAuthHttpMethod, OAuthHttpRequest, OAuthHttpResponse, OAuthPendingAuthorization,
79    OAuthPendingAuthorizationState, OAuthRedirectPolicy, OAuthStoredToken, OAuthTokenBinding,
80    OAuthTokenStore, ReqwestOAuthHttpClient,
81};
82pub use response_cache::{ClientCacheConfig, DEFAULT_MAX_CACHE_TTL};
83pub use stdio::StdioClientTransport;
84pub use transport::ClientTransport;
85#[cfg(feature = "websocket")]
86pub use websocket::{WebSocketClientConfig, WebSocketClientTransport};
87
88use std::collections::HashMap;
89use std::sync::Arc;
90use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
91
92use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
93use tokio::task::JoinHandle;
94
95use crate::ProtocolSupport;
96use crate::error::{Error, ErrorCode, McpErrorCode, Result};
97#[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
98use crate::protocol::DiscoverParams;
99use crate::protocol::{
100    CacheScope, CallToolParams, CallToolResult, CancelTaskParams, CancelledParams,
101    ClientCapabilities, CompleteParams, CompleteResult, CompletionArgument, CompletionReference,
102    CreateTaskResult, DiscoverResult, ElicitationCapability, GetPromptParams, GetPromptResult,
103    GetTaskInfoParams, Implementation, InitializeParams, InitializeResult, InputRequest,
104    InputRequests, InputResponse, InputResponses, JsonRpcNotification, JsonRpcRequest,
105    ListPromptsParams, ListPromptsResult, ListResourceTemplatesParams, ListResourceTemplatesResult,
106    ListResourcesParams, ListResourcesResult, ListRootsResult, ListToolsParams, ListToolsResult,
107    PromptDefinition, ReadResourceParams, ReadResourceResult, RequestId, RequestMeta,
108    RequestOutcome, ResourceDefinition, ResourceTemplateDefinition, Root, RootsCapability,
109    SamplingCapability, SubscriptionFilter, SubscriptionsAcknowledgedParams,
110    SubscriptionsListenParams, SubscriptionsListenResult, TaskObject, TaskRequestParams,
111    TaskStatusParams, ToolDefinition, UpdateTaskParams, notifications,
112};
113use response_cache::{CacheLookup, ClientResponseCache};
114use tower_mcp_types::JsonRpcError;
115
116/// One response to a final-protocol `tools/call` request.
117///
118/// Task creation is server-directed in SEP-2663, so a client that declares
119/// the Tasks extension must be prepared for an ordinary tool call to return a
120/// task handle. [`McpClient::call_tool`] drives that task transparently;
121/// [`McpClient::call_tool_once_task_aware`] exposes this enum for callers that
122/// want direct control of the lifecycle.
123#[derive(Debug, Clone, serde::Deserialize)]
124#[serde(untagged)]
125#[non_exhaustive]
126pub enum TaskAwareCallToolOutcome {
127    /// The server elected to create a task.
128    Task(crate::tasks::CreateTaskResult),
129    /// The request completed synchronously.
130    Complete(CallToolResult),
131    /// The request needs one or more client inputs before it can complete.
132    InputRequired(crate::protocol::InputRequiredResult),
133}
134
135trait CacheableResponse:
136    Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static
137{
138    fn ttl_ms(&self) -> Option<u64>;
139    fn cache_scope(&self) -> Option<CacheScope>;
140}
141
142macro_rules! impl_cacheable_response {
143    ($($ty:ty),+ $(,)?) => {
144        $(
145            impl CacheableResponse for $ty {
146                fn ttl_ms(&self) -> Option<u64> {
147                    self.ttl_ms
148                }
149
150                fn cache_scope(&self) -> Option<CacheScope> {
151                    self.cache_scope
152                }
153            }
154        )+
155    };
156}
157
158impl_cacheable_response!(
159    DiscoverResult,
160    ListToolsResult,
161    ListResourcesResult,
162    ListResourceTemplatesResult,
163    ListPromptsResult,
164    ReadResourceResult,
165);
166
167/// Cancels an in-flight request if its caller's future is dropped.
168///
169/// Disarmed as soon as the response arrives, so only an abandoned request is
170/// cancelled. Uses `try_send` because `Drop` cannot await: if the command
171/// channel is full or closed the cancellation is skipped rather than blocking
172/// a drop, which is the same trade-off `SubscriptionHandle` already makes.
173struct CancelOnDrop {
174    id: RequestId,
175    command_tx: mpsc::Sender<LoopCommand>,
176    armed: bool,
177}
178
179impl Drop for CancelOnDrop {
180    fn drop(&mut self) {
181        if self.armed {
182            let _ = self.command_tx.try_send(LoopCommand::CancelRequest {
183                request_id: self.id.clone(),
184                done_tx: None,
185            });
186        }
187    }
188}
189
190/// Internal command sent from McpClient methods to the background loop.
191enum LoopCommand {
192    /// Send a JSON-RPC request and await a response.
193    ///
194    /// The id is allocated by the caller rather than the loop, so a caller
195    /// can cancel the request it sent (#1202). The loop previously owned the
196    /// counter, which left callers with no way to name their own request.
197    Request {
198        id: RequestId,
199        method: String,
200        params: serde_json::Value,
201        response_tx: oneshot::Sender<Result<serde_json::Value>>,
202    },
203    /// Open a long-lived `subscriptions/listen` request and return its ID.
204    StartSubscription {
205        params: serde_json::Value,
206        id_tx: oneshot::Sender<RequestId>,
207        acknowledgment_tx: oneshot::Sender<SubscriptionFilter>,
208        response_tx: oneshot::Sender<Result<serde_json::Value>>,
209    },
210    /// Cancel one in-flight request, of any method.
211    CancelRequest {
212        request_id: RequestId,
213        done_tx: Option<oneshot::Sender<Result<()>>>,
214    },
215    /// Send a JSON-RPC notification (no JSON-RPC response expected; the
216    /// completion channel reports whether the transport delivered it).
217    Notify {
218        method: String,
219        params: serde_json::Value,
220        done_tx: oneshot::Sender<Result<()>>,
221    },
222    /// Reset the transport's session state for re-initialization.
223    ResetSession { done_tx: oneshot::Sender<()> },
224    /// Fulfil embedded MRTR requests through the configured client handler.
225    ResolveInputs {
226        requests: InputRequests,
227        response_tx: oneshot::Sender<Result<InputResponses>>,
228    },
229    /// Graceful shutdown.
230    Shutdown,
231}
232
233/// An active `subscriptions/listen` request.
234///
235/// The handle exposes the JSON-RPC request ID used as the subscription ID,
236/// the server's acknowledged filter, graceful server completion, and explicit
237/// cancellation. Dropping an active handle requests cancellation on a
238/// best-effort basis; callers that need confirmation should call
239/// [`cancel()`](Self::cancel).
240#[must_use = "dropping the handle cancels the active subscription"]
241pub struct SubscriptionHandle {
242    request_id: RequestId,
243    command_tx: mpsc::Sender<LoopCommand>,
244    acknowledgment_rx: Option<oneshot::Receiver<SubscriptionFilter>>,
245    response_rx: Option<oneshot::Receiver<Result<serde_json::Value>>>,
246    active: bool,
247}
248
249impl SubscriptionHandle {
250    /// The JSON-RPC request ID that identifies this subscription.
251    pub fn id(&self) -> &RequestId {
252        &self.request_id
253    }
254
255    /// Wait for the server's mandatory first-message acknowledgment.
256    ///
257    /// The returned filter is the subset the server agreed to honor.
258    pub async fn acknowledged(&mut self) -> Result<SubscriptionFilter> {
259        let receiver = self.acknowledgment_rx.take().ok_or_else(|| {
260            Error::Transport("subscription acknowledgment was already consumed".to_string())
261        })?;
262        receiver
263            .await
264            .map_err(|_| Error::Transport("subscription ended before acknowledgment".to_string()))
265    }
266
267    /// Wait for the server to end the subscription gracefully.
268    ///
269    /// An HTTP disconnect without a terminal response is reported as a
270    /// transport error. Dropping this future drops the handle and cancels the
271    /// subscription.
272    pub async fn wait(mut self) -> Result<SubscriptionsListenResult> {
273        let receiver = self.response_rx.take().ok_or_else(|| {
274            Error::Transport("subscription result was already consumed".to_string())
275        })?;
276        let value = receiver
277            .await
278            .map_err(|_| Error::Transport("connection closed".to_string()))??;
279        self.active = false;
280        let result: SubscriptionsListenResult = serde_json::from_value(value).map_err(|error| {
281            Error::Transport(format!(
282                "failed to deserialize subscriptions/listen response: {error}"
283            ))
284        })?;
285        if !result.result_type.is_complete() {
286            return Err(Error::Transport(format!(
287                "subscriptions/listen ended with unexpected result type {:?}",
288                result.result_type
289            )));
290        }
291        if !request_ids_match(&result.meta.subscription_id, &self.request_id) {
292            return Err(Error::Transport(
293                "subscriptions/listen result carried the wrong subscription ID".to_string(),
294            ));
295        }
296        Ok(result)
297    }
298
299    /// Cancel the subscription and wait until its transport stream is closed.
300    pub async fn cancel(mut self) -> Result<()> {
301        let (done_tx, done_rx) = oneshot::channel();
302        self.command_tx
303            .send(LoopCommand::CancelRequest {
304                request_id: self.request_id.clone(),
305                done_tx: Some(done_tx),
306            })
307            .await
308            .map_err(|_| Error::Transport("connection closed".to_string()))?;
309        let result = done_rx
310            .await
311            .map_err(|_| Error::Transport("connection closed".to_string()))?;
312        self.active = false;
313        result
314    }
315}
316
317impl Drop for SubscriptionHandle {
318    fn drop(&mut self) {
319        if self.active {
320            let _ = self.command_tx.try_send(LoopCommand::CancelRequest {
321                request_id: self.request_id.clone(),
322                done_tx: None,
323            });
324        }
325    }
326}
327
328/// MCP client with a background message loop.
329///
330/// Unlike previous versions, this type is not generic over the transport.
331/// The transport is consumed during [`connect()`](Self::connect) and moved
332/// into a background Tokio task that handles message multiplexing.
333///
334/// All public methods take `&self`, enabling concurrent use from multiple
335/// tasks.
336///
337/// # Construction
338///
339/// ```rust,no_run
340/// use tower_mcp::client::{McpClient, StdioClientTransport};
341///
342/// # async fn example() -> Result<(), tower_mcp::BoxError> {
343/// // Simple: no handler for server-initiated requests
344/// let transport = StdioClientTransport::spawn("server", &[]).await?;
345/// let client = McpClient::connect(transport).await?;
346///
347/// // With configuration
348/// use tower_mcp::protocol::Root;
349/// let transport = StdioClientTransport::spawn("server", &[]).await?;
350/// let client = McpClient::builder()
351///     .with_roots(vec![Root::new("file:///project")])
352///     .connect_simple(transport)
353///     .await?;
354/// # Ok(())
355/// # }
356/// ```
357pub struct McpClient {
358    /// Channel to send commands to the background loop.
359    command_tx: mpsc::Sender<LoopCommand>,
360    /// Background task handle.
361    task: Option<JoinHandle<()>>,
362    /// Whether `initialize()` has been called successfully.
363    initialized: AtomicBool,
364    /// Server info (set after successful initialization).
365    server_info: RwLock<Option<InitializeResult>>,
366    /// Client capabilities declared during initialization.
367    capabilities: ClientCapabilities,
368    /// Exact ordered set of protocol implementations enabled for this client.
369    protocol_support: ProtocolSupport,
370    /// Protocol selected by the discover-based final lifecycle.
371    selected_protocol_version: RwLock<Option<String>>,
372    /// Client identity repeated in final-protocol request metadata.
373    client_info: RwLock<Option<Implementation>>,
374    /// Server discovery result, when the final lifecycle is active.
375    discovery: RwLock<Option<DiscoverResult>>,
376    /// Current roots (shared with the loop for roots/list responses).
377    roots: Arc<RwLock<Vec<Root>>>,
378    /// Whether the transport is still connected.
379    connected: Arc<AtomicBool>,
380    /// Whether the transport supports session recovery.
381    supports_session_recovery: bool,
382    /// Stored init params for session recovery re-initialization.
383    init_params: RwLock<Option<(String, String)>>,
384    /// Lock to prevent concurrent session recovery attempts.
385    recovery_lock: Mutex<()>,
386    /// Maximum number of input-required rounds auto-driven per operation.
387    max_mrtr_rounds: usize,
388    /// SEP-2549 final-protocol response cache.
389    response_cache: Arc<ClientResponseCache>,
390    /// Allocator for per-request progress tokens; `None` when the client did
391    /// not opt into progress.
392    progress_tokens: Option<Arc<AtomicI64>>,
393    /// Allocator for JSON-RPC request ids, shared with the message loop so
394    /// caller-issued and loop-issued ids never collide.
395    next_request_id: Arc<AtomicI64>,
396}
397
398/// Settings a builder hands to the connect path, grouped so the private
399/// constructor keeps one parameter per concern rather than one per field.
400struct ClientSettings {
401    capabilities: ClientCapabilities,
402    roots: Vec<Root>,
403    protocol_support: ProtocolSupport,
404    max_mrtr_rounds: usize,
405    cache_config: ClientCacheConfig,
406    request_progress: bool,
407}
408
409/// Builder for configuring and connecting an [`McpClient`].
410///
411/// # Example
412///
413/// ```rust,no_run
414/// use tower_mcp::client::{McpClient, StdioClientTransport};
415/// use tower_mcp::protocol::Root;
416///
417/// # async fn example() -> Result<(), tower_mcp::BoxError> {
418/// let transport = StdioClientTransport::spawn("server", &[]).await?;
419/// let handler = (); // Use a real ClientHandler for bidirectional support
420/// let client = McpClient::builder()
421///     .with_roots(vec![Root::new("file:///project")])
422///     .with_sampling()
423///     .connect(transport, handler)
424///     .await?;
425/// # Ok(())
426/// # }
427/// ```
428pub struct McpClientBuilder {
429    capabilities: ClientCapabilities,
430    roots: Vec<Root>,
431    protocol_support: ProtocolSupport,
432    max_mrtr_rounds: usize,
433    cache_config: ClientCacheConfig,
434    request_progress: bool,
435}
436
437impl McpClientBuilder {
438    /// Create a new builder with default settings.
439    pub fn new() -> Self {
440        Self {
441            capabilities: ClientCapabilities::default(),
442            roots: Vec::new(),
443            // Every compiled implementation, matching servers (#1179). The
444            // entry point still selects the era: nothing calls `discover`
445            // implicitly, so compiling a feature never changes an existing
446            // client's wire behavior, it only removes the configuration step
447            // before `discover`.
448            protocol_support: ProtocolSupport::default(),
449            max_mrtr_rounds: 8,
450            cache_config: ClientCacheConfig::default(),
451            request_progress: false,
452        }
453    }
454
455    /// Ask servers to report progress for this client's requests.
456    ///
457    /// A server only emits progress when the request carries a progress
458    /// token, so [`RequestContext::report_progress`] is a no-op for a client
459    /// that never sends one. With this enabled, every request this client
460    /// issues carries a fresh token and the matching
461    /// `notifications/progress` frames reach the handler's
462    /// [`on_progress`](crate::client::NotificationHandler::on_progress)
463    /// callback.
464    ///
465    /// Off by default: a token asks the server to do extra work, and a client
466    /// with no progress handler would only add wire traffic.
467    ///
468    /// [`RequestContext::report_progress`]: crate::context::RequestContext::report_progress
469    pub fn request_progress(mut self) -> Self {
470        self.request_progress = true;
471        self
472    }
473
474    /// Configure roots for this client.
475    ///
476    /// The client will declare roots support during initialization and
477    /// respond to `roots/list` requests with these roots.
478    pub fn with_roots(mut self, roots: Vec<Root>) -> Self {
479        self.roots = roots;
480        self.capabilities.roots = Some(RootsCapability {
481            list_changed: true,
482            deprecated: None,
483        });
484        self
485    }
486
487    /// Configure custom capabilities for this client.
488    pub fn with_capabilities(mut self, capabilities: ClientCapabilities) -> Self {
489        self.capabilities = capabilities;
490        self
491    }
492
493    /// Add one validated MCP protocol-extension declaration.
494    ///
495    /// Repeated declarations for the same identifier use last-write-wins
496    /// semantics. Other configured capabilities are preserved.
497    pub fn with_protocol_extension(mut self, extension: crate::ExtensionDeclaration) -> Self {
498        let (identifier, settings) = extension.into_parts();
499        self.capabilities
500            .extensions
501            .get_or_insert_default()
502            .insert(identifier, settings);
503        self
504    }
505
506    /// Set the exact ordered protocol versions enabled for this client.
507    ///
508    /// The default is [`ProtocolSupport::default`], every implementation
509    /// compiled into this build: with `protocol-2026-07-28` enabled the
510    /// client can call `McpClient::discover` without further configuration,
511    /// and without the feature only the stable session protocols exist.
512    /// Pass [`ProtocolSupport::stable`] to keep a feature-enabled build on
513    /// the session protocols only.
514    pub fn protocol_support(mut self, support: ProtocolSupport) -> Self {
515        self.protocol_support = support;
516        self
517    }
518
519    /// Bound the number of MRTR rounds automatically followed for one request.
520    ///
521    /// Zero is normalized to one. The default is eight rounds.
522    pub fn max_mrtr_rounds(mut self, rounds: usize) -> Self {
523        self.max_mrtr_rounds = rounds.max(1);
524        self
525    }
526
527    /// Configure the SEP-2549 final-protocol response cache.
528    pub fn response_cache(mut self, config: ClientCacheConfig) -> Self {
529        self.cache_config = config;
530        self
531    }
532
533    /// Disable the SEP-2549 response cache.
534    pub fn disable_response_cache(mut self) -> Self {
535        self.cache_config.enabled = false;
536        self
537    }
538
539    /// Declare sampling support.
540    ///
541    /// Sets the sampling capability so the server knows this client can
542    /// handle `sampling/createMessage` requests. The handler passed to
543    /// [`connect()`](Self::connect) should override
544    /// [`handle_create_message()`](ClientHandler::handle_create_message).
545    pub fn with_sampling(mut self) -> Self {
546        self.capabilities.sampling = Some(SamplingCapability::default());
547        self
548    }
549
550    /// Declare elicitation support.
551    ///
552    /// Sets the elicitation capability so the server knows this client can
553    /// handle `elicitation/create` requests. The handler passed to
554    /// [`connect()`](Self::connect) should override
555    /// [`handle_elicit()`](ClientHandler::handle_elicit).
556    pub fn with_elicitation(mut self) -> Self {
557        self.capabilities.elicitation = Some(ElicitationCapability::default());
558        self
559    }
560
561    /// Connect to a server using the given transport and handler.
562    ///
563    /// Spawns a background task to handle message I/O. The transport is
564    /// consumed and owned by the background task.
565    pub async fn connect<T, H>(self, transport: T, handler: H) -> Result<McpClient>
566    where
567        T: ClientTransport,
568        H: ClientHandler,
569    {
570        McpClient::connect_inner(
571            transport,
572            handler,
573            ClientSettings {
574                capabilities: self.capabilities,
575                roots: self.roots,
576                protocol_support: self.protocol_support,
577                max_mrtr_rounds: self.max_mrtr_rounds,
578                cache_config: self.cache_config,
579                request_progress: self.request_progress,
580            },
581        )
582        .await
583    }
584
585    /// Connect to a server without a handler.
586    ///
587    /// All server-initiated requests will be rejected with `method_not_found`.
588    pub async fn connect_simple<T: ClientTransport>(self, transport: T) -> Result<McpClient> {
589        self.connect(transport, ()).await
590    }
591}
592
593impl Default for McpClientBuilder {
594    fn default() -> Self {
595        Self::new()
596    }
597}
598
599impl McpClient {
600    /// Connect with default settings and no handler.
601    ///
602    /// Shorthand for `McpClient::builder().connect_simple(transport)`.
603    pub async fn connect<T: ClientTransport>(transport: T) -> Result<Self> {
604        McpClientBuilder::new().connect_simple(transport).await
605    }
606
607    /// Connect with a handler for server-initiated requests.
608    pub async fn connect_with_handler<T, H>(transport: T, handler: H) -> Result<Self>
609    where
610        T: ClientTransport,
611        H: ClientHandler,
612    {
613        McpClientBuilder::new().connect(transport, handler).await
614    }
615
616    /// Create a builder for advanced configuration.
617    pub fn builder() -> McpClientBuilder {
618        McpClientBuilder::new()
619    }
620
621    /// Internal connect implementation.
622    async fn connect_inner<T, H>(transport: T, handler: H, settings: ClientSettings) -> Result<Self>
623    where
624        T: ClientTransport,
625        H: ClientHandler,
626    {
627        let ClientSettings {
628            capabilities,
629            roots,
630            protocol_support,
631            max_mrtr_rounds,
632            cache_config,
633            request_progress,
634        } = settings;
635        let supports_session_recovery = transport.supports_session_recovery();
636        let (command_tx, command_rx) = mpsc::channel::<LoopCommand>(64);
637        let connected = Arc::new(AtomicBool::new(true));
638        let roots = Arc::new(RwLock::new(roots));
639        let response_cache = ClientResponseCache::new(cache_config);
640
641        let loop_connected = connected.clone();
642        let loop_roots = roots.clone();
643        let loop_response_cache = response_cache.clone();
644        let next_request_id = Arc::new(AtomicI64::new(1));
645        let loop_next_id = next_request_id.clone();
646
647        let task = tokio::spawn(async move {
648            message_loop(
649                transport,
650                handler,
651                command_rx,
652                loop_connected,
653                loop_roots,
654                loop_response_cache,
655                loop_next_id,
656            )
657            .await;
658        });
659
660        Ok(Self {
661            command_tx,
662            task: Some(task),
663            initialized: AtomicBool::new(false),
664            server_info: RwLock::new(None),
665            capabilities,
666            protocol_support,
667            selected_protocol_version: RwLock::new(None),
668            client_info: RwLock::new(None),
669            discovery: RwLock::new(None),
670            roots,
671            connected,
672            supports_session_recovery,
673            init_params: RwLock::new(None),
674            recovery_lock: Mutex::new(()),
675            max_mrtr_rounds,
676            response_cache,
677            progress_tokens: request_progress.then(|| Arc::new(AtomicI64::new(1))),
678            next_request_id,
679        })
680    }
681
682    /// Check if the client has been initialized.
683    pub fn is_initialized(&self) -> bool {
684        self.initialized.load(Ordering::Acquire)
685    }
686
687    /// Check if the transport is still connected.
688    pub fn is_connected(&self) -> bool {
689        self.connected.load(Ordering::Acquire)
690    }
691
692    /// Get the server info (available after initialization).
693    pub async fn server_info(&self) -> Option<InitializeResult> {
694        self.server_info.read().await.clone()
695    }
696
697    /// Return the exact ordered protocol implementations enabled for this client.
698    pub fn protocol_support(&self) -> &ProtocolSupport {
699        &self.protocol_support
700    }
701
702    /// Clear every cached final-protocol response held by this client.
703    pub async fn clear_response_cache(&self) {
704        self.response_cache.clear().await;
705    }
706
707    /// Change the authorization-context partition used for private responses.
708    ///
709    /// Previously cached private entries become inaccessible, while public
710    /// entries remain reusable. Call this before issuing requests after the
711    /// authenticated principal changes.
712    pub async fn set_cache_partition(&self, partition: impl Into<String>) {
713        self.response_cache.set_partition(partition.into()).await;
714    }
715
716    /// Return the number of response-cache entries held by this client.
717    pub async fn response_cache_len(&self) -> usize {
718        self.response_cache.len().await
719    }
720
721    /// Get the server discovery result after the final lifecycle is active.
722    pub async fn discovery(&self) -> Option<DiscoverResult> {
723        self.discovery.read().await.clone()
724    }
725
726    /// Get the protocol version selected for the discover-based lifecycle.
727    pub async fn selected_protocol_version(&self) -> Option<String> {
728        self.selected_protocol_version.read().await.clone()
729    }
730
731    /// Get the server info synchronously (best-effort, non-blocking).
732    ///
733    /// Returns `None` if the lock is currently held by a writer or if
734    /// initialization hasn't completed. Prefer [`server_info()`](Self::server_info)
735    /// in async contexts.
736    pub fn server_info_blocking(&self) -> Option<InitializeResult> {
737        self.server_info.try_read().ok()?.clone()
738    }
739
740    /// Initialize the MCP connection.
741    ///
742    /// Sends the `initialize` request and `notifications/initialized` notification.
743    /// Must be called before any other operations.
744    pub async fn initialize(
745        &self,
746        client_name: &str,
747        client_version: &str,
748    ) -> Result<InitializeResult> {
749        let params = InitializeParams {
750            protocol_version: crate::protocol::LATEST_PROTOCOL_VERSION.to_string(),
751            capabilities: self.capabilities.clone(),
752            client_info: Implementation {
753                name: client_name.to_string(),
754                version: client_version.to_string(),
755                ..Default::default()
756            },
757            meta: None,
758        };
759
760        let result: InitializeResult = self.send_request("initialize", &params).await?;
761        *self.server_info.write().await = Some(result.clone());
762
763        // Store init params for potential session recovery
764        *self.init_params.write().await =
765            Some((client_name.to_string(), client_version.to_string()));
766
767        // Send initialized notification. A delivery failure is an
768        // initialization failure: the server will reject every subsequent
769        // request until the notification arrives.
770        self.send_notification("notifications/initialized", &serde_json::json!({}))
771            .await
772            .map_err(|error| {
773                Error::Transport(format!(
774                    "failed to deliver notifications/initialized: {error}"
775                ))
776            })?;
777        self.initialized.store(true, Ordering::Release);
778
779        Ok(result)
780    }
781
782    /// Start the sessionless 2026-07-28 lifecycle with `server/discover`.
783    ///
784    /// This path is available only when the final implementation was compiled.
785    /// The client sends required per-request metadata from the first request,
786    /// retries one `Unsupported protocol version` response using the server's
787    /// advertised intersection, and then repeats the selected version,
788    /// capabilities, and client identity on every subsequent request.
789    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
790    pub async fn discover(
791        &self,
792        client_name: &str,
793        client_version: &str,
794    ) -> Result<DiscoverResult> {
795        use crate::protocol::PROTOCOL_VERSION_2026_07_28;
796
797        let client_info = Implementation {
798            name: client_name.to_string(),
799            version: client_version.to_string(),
800            ..Default::default()
801        };
802        *self.client_info.write().await = Some(client_info.clone());
803
804        let mut candidate = self
805            .protocol_support
806            .versions()
807            .iter()
808            .find(|version| version.as_str() == PROTOCOL_VERSION_2026_07_28)
809            .cloned()
810            .ok_or_else(|| {
811                Error::Transport(
812                    "2026-07-28 is not enabled for this client; configure ProtocolSupport"
813                        .to_string(),
814                )
815            })?;
816        let mut retried_unsupported = false;
817
818        loop {
819            let params = DiscoverParams {
820                meta: Some(self.request_meta_for(&candidate, &client_info)),
821            };
822            let cache_key = serde_json::to_string(&(
823                client_name,
824                client_version,
825                candidate.as_str(),
826                &self.capabilities,
827            ))
828            .expect("discovery cache key is serializable");
829            match self
830                .send_cacheable_request_when::<_, DiscoverResult>(
831                    "server/discover",
832                    &cache_key,
833                    &params,
834                    true,
835                )
836                .await
837            {
838                Ok(result) => {
839                    let selected = self
840                        .protocol_support
841                        .versions()
842                        .iter()
843                        .find(|version| {
844                            result
845                                .supported_versions
846                                .iter()
847                                .any(|supported| supported == *version)
848                        })
849                        .cloned()
850                        .ok_or_else(|| {
851                            Error::Transport(format!(
852                                "server and client have no protocol version in common; server: {:?}, client: {:?}",
853                                result.supported_versions,
854                                self.protocol_support.versions()
855                            ))
856                        })?;
857                    *self.selected_protocol_version.write().await = Some(selected);
858                    *self.discovery.write().await = Some(result.clone());
859                    self.initialized.store(true, Ordering::Release);
860                    return Ok(result);
861                }
862                Err(Error::JsonRpc(error)) if error.code == -32022 && !retried_unsupported => {
863                    let supported = error
864                        .data
865                        .as_ref()
866                        .and_then(|data| data.get("supported"))
867                        .and_then(serde_json::Value::as_array)
868                        .ok_or_else(|| Error::JsonRpc(error.clone()))?;
869                    candidate = self
870                        .protocol_support
871                        .versions()
872                        .iter()
873                        .find(|version| {
874                            supported
875                                .iter()
876                                .any(|item| item.as_str() == Some(version.as_str()))
877                        })
878                        .cloned()
879                        .ok_or_else(|| Error::JsonRpc(error.clone()))?;
880                    retried_unsupported = true;
881                }
882                Err(error) => return Err(error),
883            }
884        }
885    }
886
887    /// List available tools.
888    pub async fn list_tools(&self) -> Result<ListToolsResult> {
889        self.list_tools_with_cursor(None).await
890    }
891
892    /// Call a tool.
893    ///
894    /// On the final lifecycle, a header mismatch, method-not-found, or
895    /// invalid-params response can indicate that the cached tool schema is
896    /// stale. The client invalidates `tools/list`, refreshes it, and retries
897    /// the rejected round once. These errors are raised before tool execution,
898    /// so the bounded retry does not replay a completed side effect.
899    pub async fn call_tool(
900        &self,
901        name: &str,
902        arguments: serde_json::Value,
903    ) -> Result<CallToolResult> {
904        let mut input_responses = None;
905        let mut request_state = None;
906        let mut schema_retry_available = self.uses_final_protocol().await;
907        for round in 0..=self.max_mrtr_rounds {
908            let params = CallToolParams {
909                name: name.to_string(),
910                arguments: arguments.clone(),
911                input_responses: input_responses.take(),
912                request_state: request_state.take(),
913                meta: None,
914                task: None,
915            };
916            let outcome = self
917                .send_task_aware_tool_request_with_schema_retry(
918                    &params,
919                    &mut schema_retry_available,
920                )
921                .await?;
922            match outcome {
923                TaskAwareCallToolOutcome::Complete(result) => return Ok(result),
924                TaskAwareCallToolOutcome::Task(created) => {
925                    return self
926                        .complete_final_task(&created.task.metadata.task_id)
927                        .await;
928                }
929                TaskAwareCallToolOutcome::InputRequired(required) => {
930                    if round == self.max_mrtr_rounds {
931                        return Err(Error::Transport(format!(
932                            "MRTR round limit ({}) exceeded for tools/call",
933                            self.max_mrtr_rounds
934                        )));
935                    }
936                    let requests = required.input_requests.ok_or_else(|| {
937                        Error::Transport(
938                            "input_required result has no requests the client can fulfil"
939                                .to_string(),
940                        )
941                    })?;
942                    input_responses = Some(self.resolve_input_requests(requests).await?);
943                    request_state = required.request_state;
944                }
945            }
946        }
947        unreachable!("MRTR loop either completes or returns at the configured bound")
948    }
949
950    /// Send one tools/call attempt without automatically following MRTR input.
951    pub async fn call_tool_once(
952        &self,
953        name: &str,
954        arguments: serde_json::Value,
955        input_responses: Option<InputResponses>,
956        request_state: Option<String>,
957    ) -> Result<RequestOutcome<CallToolResult>> {
958        match self
959            .call_tool_once_task_aware(name, arguments, input_responses, request_state)
960            .await?
961        {
962            TaskAwareCallToolOutcome::Complete(result) => Ok(RequestOutcome::Complete(result)),
963            TaskAwareCallToolOutcome::InputRequired(required) => {
964                Ok(RequestOutcome::InputRequired(required))
965            }
966            TaskAwareCallToolOutcome::Task(created) => Err(Error::Transport(format!(
967                "tools/call returned task '{}'; use call_tool_once_task_aware for direct task lifecycle control",
968                created.task.metadata.task_id
969            ))),
970        }
971    }
972
973    /// Send one `tools/call` attempt and preserve a server-created task.
974    ///
975    /// Unlike [`call_tool`](Self::call_tool), this does not poll a task or
976    /// automatically fulfil input requests. Final-protocol callers can use it
977    /// to retain the exact task handle returned from the ordinary request.
978    pub async fn call_tool_once_task_aware(
979        &self,
980        name: &str,
981        arguments: serde_json::Value,
982        input_responses: Option<InputResponses>,
983        request_state: Option<String>,
984    ) -> Result<TaskAwareCallToolOutcome> {
985        self.ensure_initialized()?;
986        let params = CallToolParams {
987            name: name.to_string(),
988            arguments,
989            input_responses,
990            request_state,
991            meta: None,
992            task: None,
993        };
994        let mut schema_retry_available = self.uses_final_protocol().await;
995        self.send_task_aware_tool_request_with_schema_retry(&params, &mut schema_retry_available)
996            .await
997    }
998
999    /// Request direct control of a tool task lifecycle.
1000    ///
1001    /// Instead of blocking until the tool finishes, the server creates a
1002    /// task and immediately returns a [`CreateTaskResult`] carrying the task
1003    /// id. Poll with [`task_get`](Self::task_get) or block with
1004    /// [`task_wait`](Self::task_wait); a completed task's `result` field
1005    /// carries the [`CallToolResult`] the synchronous call would have
1006    /// returned.
1007    ///
1008    /// On 2025-11-25, `ttl_ms` is sent in the legacy task-augmentation field.
1009    /// On 2026-07-28, task creation is server-directed: this sends an ordinary
1010    /// request and requires the server to elect a task. A final client cannot
1011    /// request a TTL, so a non-`None` `ttl_ms` is rejected on that lifecycle.
1012    pub async fn call_tool_as_task(
1013        &self,
1014        name: &str,
1015        arguments: serde_json::Value,
1016        ttl_ms: Option<u64>,
1017    ) -> Result<CreateTaskResult> {
1018        self.ensure_initialized()?;
1019        if self.uses_final_protocol().await {
1020            if ttl_ms.is_some() {
1021                return Err(Error::Transport(
1022                    "ttl_ms is server-selected by the final Tasks extension".to_string(),
1023                ));
1024            }
1025            let params = CallToolParams {
1026                name: name.to_string(),
1027                arguments,
1028                input_responses: None,
1029                request_state: None,
1030                meta: None,
1031                task: None,
1032            };
1033            let mut schema_retry_available = true;
1034            return match self
1035                .send_task_aware_tool_request_with_schema_retry(
1036                    &params,
1037                    &mut schema_retry_available,
1038                )
1039                .await?
1040            {
1041                TaskAwareCallToolOutcome::Task(created) => {
1042                    Ok(Self::legacy_create_task_from_final(created))
1043                }
1044                TaskAwareCallToolOutcome::Complete(_) => Err(Error::Transport(
1045                    "server completed tools/call synchronously; final task creation is server-directed"
1046                        .to_string(),
1047                )),
1048                TaskAwareCallToolOutcome::InputRequired(_) => Err(Error::Transport(
1049                    "server requested input instead of creating a task".to_string(),
1050                )),
1051            };
1052        }
1053
1054        let params = CallToolParams {
1055            name: name.to_string(),
1056            arguments,
1057            input_responses: None,
1058            request_state: None,
1059            meta: None,
1060            task: Some(TaskRequestParams { ttl: ttl_ms }),
1061        };
1062        let mut schema_retry_available = self.uses_final_protocol().await;
1063        self.send_tool_request_with_schema_retry(&params, &mut schema_retry_available)
1064            .await
1065    }
1066
1067    /// Fetch a task's current state via `tasks/get` (SEP-2663).
1068    ///
1069    /// For `completed` tasks the returned object carries the terminal
1070    /// [`CallToolResult`] in its `result` field; for `failed` tasks the
1071    /// JSON-RPC error is in `error`. Unknown or expired task ids surface as
1072    /// an invalid-params error from the server.
1073    pub async fn task_get(&self, task_id: &str) -> Result<TaskObject> {
1074        self.ensure_initialized()?;
1075        if self.uses_final_protocol().await {
1076            return Self::legacy_task_from_final(self.task_get_detailed(task_id).await?);
1077        }
1078        let params = GetTaskInfoParams {
1079            task_id: task_id.to_string(),
1080            meta: None,
1081        };
1082        self.send_request("tasks/get", &params).await
1083    }
1084
1085    /// Fetch the exact final-protocol `tasks/get` result.
1086    ///
1087    /// This preserves status-specific payloads, including all outstanding
1088    /// `inputRequests`. It is available only after selecting the 2026-07-28
1089    /// lifecycle; legacy callers should use [`task_get`](Self::task_get).
1090    pub async fn task_get_detailed(&self, task_id: &str) -> Result<crate::tasks::GetTaskResult> {
1091        self.ensure_initialized()?;
1092        if !self.uses_final_protocol().await {
1093            return Err(Error::Transport(
1094                "task_get_detailed requires the 2026-07-28 client lifecycle".to_string(),
1095            ));
1096        }
1097        let params = crate::tasks::GetTaskParams {
1098            task_id: task_id.to_string(),
1099            meta: None,
1100        };
1101        self.send_request("tasks/get", &params).await
1102    }
1103
1104    /// Cancel a task via `tasks/cancel` (SEP-2663).
1105    ///
1106    /// Cancellation is cooperative: the acknowledgment is an empty result
1107    /// and the observable status may remain non-terminal for a while after
1108    /// the ack; poll [`task_get`](Self::task_get) to observe the terminal
1109    /// state. `reason` is a legacy-only field and is omitted on the final
1110    /// protocol. The ack body is discarded, so legacy peers that return the
1111    /// task object are also tolerated.
1112    pub async fn task_cancel(&self, task_id: &str, reason: Option<String>) -> Result<()> {
1113        self.ensure_initialized()?;
1114        if self.uses_final_protocol().await {
1115            let params = crate::tasks::CancelTaskParams {
1116                task_id: task_id.to_string(),
1117                meta: None,
1118            };
1119            let _ack: crate::tasks::CancelTaskResult =
1120                self.send_request("tasks/cancel", &params).await?;
1121            return Ok(());
1122        }
1123        let params = CancelTaskParams {
1124            task_id: task_id.to_string(),
1125            reason,
1126            meta: None,
1127        };
1128        let _ack: serde_json::Value = self.send_request("tasks/cancel", &params).await?;
1129        Ok(())
1130    }
1131
1132    /// Answer a task's outstanding input requests via `tasks/update`
1133    /// (SEP-2663).
1134    ///
1135    /// Responses are matched to outstanding requests by key. Final-protocol
1136    /// callers read the keys from the `inputRequests` of an `input_required`
1137    /// task returned by [`task_get_detailed`](Self::task_get_detailed).
1138    ///
1139    /// A partial map is valid and expected: requests left unanswered stay
1140    /// outstanding and the task remains `input_required` until every one is
1141    /// answered. Keys the server does not currently have outstanding, whether
1142    /// unknown, already answered, or superseded by a later request, are
1143    /// ignored rather than rejected, so replaying a stale update is safe.
1144    ///
1145    /// The acknowledgment carries no data and is discarded. Poll
1146    /// [`task_get`](Self::task_get) to observe the resulting state.
1147    pub async fn task_update(&self, task_id: &str, input_responses: InputResponses) -> Result<()> {
1148        self.ensure_initialized()?;
1149        if self.uses_final_protocol().await {
1150            let params = crate::tasks::UpdateTaskParams {
1151                task_id: task_id.to_string(),
1152                input_responses,
1153                meta: None,
1154            };
1155            let _ack: crate::tasks::UpdateTaskResult =
1156                self.send_request("tasks/update", &params).await?;
1157            return Ok(());
1158        }
1159        let input_responses = input_responses
1160            .into_iter()
1161            .map(|(key, response)| serde_json::to_value(response).map(|value| (key, value)))
1162            .collect::<std::result::Result<_, _>>()?;
1163        let params = UpdateTaskParams {
1164            task_id: task_id.to_string(),
1165            input_responses,
1166            meta: None,
1167        };
1168        let _ack: serde_json::Value = self.send_request("tasks/update", &params).await?;
1169        Ok(())
1170    }
1171
1172    /// Poll `tasks/get` until the task reaches a terminal state.
1173    ///
1174    /// Honors the server's suggested polling interval (default 1000 ms,
1175    /// clamped to 50 ms..30 s). On the final protocol it also fulfils
1176    /// `input_required` requests through the registered client handlers. A
1177    /// task purged after its TTL surfaces as the server's task-not-found
1178    /// error. Wrap in
1179    /// [`tokio::time::timeout`] to bound the overall wait.
1180    pub async fn task_wait(&self, task_id: &str) -> Result<TaskObject> {
1181        if self.uses_final_protocol().await {
1182            let result = self.wait_for_final_task(task_id).await?;
1183            return Self::legacy_task_from_final(result);
1184        }
1185        loop {
1186            let task = self.task_get(task_id).await?;
1187            if task.status.is_terminal() {
1188                return Ok(task);
1189            }
1190            let interval_ms = task.poll_interval.unwrap_or(1000).clamp(50, 30_000);
1191            tokio::time::sleep(std::time::Duration::from_millis(interval_ms)).await;
1192        }
1193    }
1194
1195    /// List available resources.
1196    pub async fn list_resources(&self) -> Result<ListResourcesResult> {
1197        self.list_resources_with_cursor(None).await
1198    }
1199
1200    /// Read a resource.
1201    pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult> {
1202        self.ensure_initialized()?;
1203        let cache_enabled = self.uses_final_protocol().await && self.response_cache.enabled();
1204        let generation = if cache_enabled {
1205            self.response_cache
1206                .capture_generation("resources/read", uri)
1207                .await
1208        } else {
1209            0
1210        };
1211        let mut generation_active = cache_enabled;
1212        let mut stale = None;
1213        if cache_enabled {
1214            match self.response_cache.lookup("resources/read", uri).await {
1215                CacheLookup::Fresh(value) => {
1216                    if let Some(result) = decode_cached(&value, "resources/read") {
1217                        self.response_cache
1218                            .release_generation("resources/read", uri)
1219                            .await;
1220                        return Ok(result);
1221                    }
1222                    self.response_cache.evict_resource(uri).await;
1223                }
1224                CacheLookup::Stale(value) => stale = Some(value),
1225                CacheLookup::Miss => {}
1226            }
1227        }
1228
1229        let mut input_responses = None;
1230        let mut request_state = None;
1231        let mut followed_input_required = false;
1232        for round in 0..=self.max_mrtr_rounds {
1233            let outcome = match self
1234                .read_resource_once(uri, input_responses.take(), request_state.take())
1235                .await
1236            {
1237                Ok(outcome) => outcome,
1238                Err(error) => {
1239                    if generation_active {
1240                        self.response_cache
1241                            .release_generation("resources/read", uri)
1242                            .await;
1243                    }
1244                    if self.response_cache.serve_stale_on_error()
1245                        && let Some(value) = stale.as_ref()
1246                        && let Some(result) = decode_cached(value, "resources/read")
1247                    {
1248                        tracing::warn!(
1249                            uri,
1250                            error = %error,
1251                            "Serving stale resources/read response after refresh failure"
1252                        );
1253                        return Ok(result);
1254                    }
1255                    return Err(error);
1256                }
1257            };
1258            match outcome {
1259                RequestOutcome::Complete(result) => {
1260                    if generation_active && !followed_input_required {
1261                        self.write_cached_response("resources/read", uri, generation, &result)
1262                            .await;
1263                    }
1264                    return Ok(result);
1265                }
1266                RequestOutcome::InputRequired(required) => {
1267                    if !followed_input_required {
1268                        followed_input_required = true;
1269                        if generation_active {
1270                            self.response_cache
1271                                .release_generation("resources/read", uri)
1272                                .await;
1273                            generation_active = false;
1274                        }
1275                    }
1276                    if round == self.max_mrtr_rounds {
1277                        return Err(Error::Transport(format!(
1278                            "MRTR round limit ({}) exceeded for resources/read",
1279                            self.max_mrtr_rounds
1280                        )));
1281                    }
1282                    let requests = required.input_requests.ok_or_else(|| {
1283                        Error::Transport(
1284                            "input_required result has no requests the client can fulfil"
1285                                .to_string(),
1286                        )
1287                    })?;
1288                    input_responses = Some(self.resolve_input_requests(requests).await?);
1289                    request_state = required.request_state;
1290                }
1291            }
1292        }
1293        unreachable!("MRTR loop either completes or returns at the configured bound")
1294    }
1295
1296    /// Send one resources/read attempt without automatically following MRTR.
1297    pub async fn read_resource_once(
1298        &self,
1299        uri: &str,
1300        input_responses: Option<InputResponses>,
1301        request_state: Option<String>,
1302    ) -> Result<RequestOutcome<ReadResourceResult>> {
1303        self.ensure_initialized()?;
1304        let params = ReadResourceParams {
1305            uri: uri.to_string(),
1306            input_responses,
1307            request_state,
1308            meta: None,
1309        };
1310        self.send_request("resources/read", &params).await
1311    }
1312
1313    /// Open a final-protocol `subscriptions/listen` notification stream.
1314    ///
1315    /// The returned handle owns the long-lived request. Use
1316    /// [`SubscriptionHandle::acknowledged`] to inspect the subset accepted by
1317    /// the server, [`SubscriptionHandle::wait`] to observe graceful server
1318    /// closure, or [`SubscriptionHandle::cancel`] to close the stream.
1319    /// Notifications continue to flow through the configured
1320    /// [`ClientHandler`] and carry their subscription ID in
1321    /// [`ServerNotification::Subscription`].
1322    pub async fn listen_subscriptions(
1323        &self,
1324        notifications: SubscriptionFilter,
1325    ) -> Result<SubscriptionHandle> {
1326        self.ensure_initialized()?;
1327        if !self.uses_final_protocol().await {
1328            return Err(Error::Transport(
1329                "subscriptions/listen requires the 2026-07-28 protocol".to_string(),
1330            ));
1331        }
1332
1333        let params = SubscriptionsListenParams {
1334            notifications: Some(notifications),
1335            meta: None,
1336        };
1337        let params = serde_json::to_value(params).map_err(|error| {
1338            Error::Transport(format!(
1339                "failed to serialize subscriptions/listen params: {error}"
1340            ))
1341        })?;
1342        let params = self.with_final_request_meta(params).await?;
1343        let (id_tx, id_rx) = oneshot::channel();
1344        let (acknowledgment_tx, acknowledgment_rx) = oneshot::channel();
1345        let (response_tx, response_rx) = oneshot::channel();
1346        self.command_tx
1347            .send(LoopCommand::StartSubscription {
1348                params,
1349                id_tx,
1350                acknowledgment_tx,
1351                response_tx,
1352            })
1353            .await
1354            .map_err(|_| Error::Transport("connection closed".to_string()))?;
1355        let request_id = id_rx
1356            .await
1357            .map_err(|_| Error::Transport("connection closed".to_string()))?;
1358
1359        Ok(SubscriptionHandle {
1360            request_id,
1361            command_tx: self.command_tx.clone(),
1362            acknowledgment_rx: Some(acknowledgment_rx),
1363            response_rx: Some(response_rx),
1364            active: true,
1365        })
1366    }
1367
1368    /// Subscribe to `notifications/resources/updated` for one resource
1369    /// (`resources/subscribe`).
1370    ///
1371    /// The updates themselves arrive through the notification handler, so a
1372    /// client that subscribes without registering
1373    /// [`NotificationHandler::on_resource_updated`] will not see them. Servers
1374    /// that support this advertise `resources.subscribe` in their
1375    /// capabilities; one that does not will reject the request.
1376    pub async fn subscribe_resource(&self, uri: &str) -> Result<()> {
1377        self.ensure_initialized()?;
1378        if self.uses_final_protocol().await {
1379            return Err(Error::Transport(
1380                "resources/subscribe was removed in 2026-07-28; use subscriptions/listen"
1381                    .to_string(),
1382            ));
1383        }
1384        let _: serde_json::Value = self
1385            .send_request("resources/subscribe", &serde_json::json!({ "uri": uri }))
1386            .await?;
1387        Ok(())
1388    }
1389
1390    /// Stop receiving updates for a resource (`resources/unsubscribe`).
1391    pub async fn unsubscribe_resource(&self, uri: &str) -> Result<()> {
1392        self.ensure_initialized()?;
1393        if self.uses_final_protocol().await {
1394            return Err(Error::Transport(
1395                "resources/unsubscribe was removed in 2026-07-28; use subscriptions/listen"
1396                    .to_string(),
1397            ));
1398        }
1399        let _: serde_json::Value = self
1400            .send_request("resources/unsubscribe", &serde_json::json!({ "uri": uri }))
1401            .await?;
1402        Ok(())
1403    }
1404
1405    /// List available prompts.
1406    pub async fn list_prompts(&self) -> Result<ListPromptsResult> {
1407        self.list_prompts_with_cursor(None).await
1408    }
1409
1410    /// List tools with an optional pagination cursor.
1411    pub async fn list_tools_with_cursor(&self, cursor: Option<String>) -> Result<ListToolsResult> {
1412        self.ensure_initialized()?;
1413        let cache_key = pagination_cache_key(cursor.as_deref());
1414        self.send_cacheable_request(
1415            "tools/list",
1416            &cache_key,
1417            &ListToolsParams { cursor, meta: None },
1418        )
1419        .await
1420    }
1421
1422    /// List resources with an optional pagination cursor.
1423    pub async fn list_resources_with_cursor(
1424        &self,
1425        cursor: Option<String>,
1426    ) -> Result<ListResourcesResult> {
1427        self.ensure_initialized()?;
1428        let cache_key = pagination_cache_key(cursor.as_deref());
1429        self.send_cacheable_request(
1430            "resources/list",
1431            &cache_key,
1432            &ListResourcesParams { cursor, meta: None },
1433        )
1434        .await
1435    }
1436
1437    /// List resource templates.
1438    pub async fn list_resource_templates(&self) -> Result<ListResourceTemplatesResult> {
1439        self.list_resource_templates_with_cursor(None).await
1440    }
1441
1442    /// List resource templates with an optional pagination cursor.
1443    pub async fn list_resource_templates_with_cursor(
1444        &self,
1445        cursor: Option<String>,
1446    ) -> Result<ListResourceTemplatesResult> {
1447        self.ensure_initialized()?;
1448        let cache_key = pagination_cache_key(cursor.as_deref());
1449        self.send_cacheable_request(
1450            "resources/templates/list",
1451            &cache_key,
1452            &ListResourceTemplatesParams { cursor, meta: None },
1453        )
1454        .await
1455    }
1456
1457    /// List prompts with an optional pagination cursor.
1458    pub async fn list_prompts_with_cursor(
1459        &self,
1460        cursor: Option<String>,
1461    ) -> Result<ListPromptsResult> {
1462        self.ensure_initialized()?;
1463        let cache_key = pagination_cache_key(cursor.as_deref());
1464        self.send_cacheable_request(
1465            "prompts/list",
1466            &cache_key,
1467            &ListPromptsParams { cursor, meta: None },
1468        )
1469        .await
1470    }
1471
1472    /// List all tools, following pagination cursors until exhausted.
1473    pub async fn list_all_tools(&self) -> Result<Vec<ToolDefinition>> {
1474        let mut all = Vec::new();
1475        let mut cursor = None;
1476        loop {
1477            let result = self.list_tools_with_cursor(cursor).await?;
1478            all.extend(result.tools);
1479            match result.next_cursor {
1480                Some(c) => cursor = Some(c),
1481                None => break,
1482            }
1483        }
1484        Ok(all)
1485    }
1486
1487    /// List all resources, following pagination cursors until exhausted.
1488    pub async fn list_all_resources(&self) -> Result<Vec<ResourceDefinition>> {
1489        let mut all = Vec::new();
1490        let mut cursor = None;
1491        loop {
1492            let result = self.list_resources_with_cursor(cursor).await?;
1493            all.extend(result.resources);
1494            match result.next_cursor {
1495                Some(c) => cursor = Some(c),
1496                None => break,
1497            }
1498        }
1499        Ok(all)
1500    }
1501
1502    /// List all resource templates, following pagination cursors until exhausted.
1503    pub async fn list_all_resource_templates(&self) -> Result<Vec<ResourceTemplateDefinition>> {
1504        let mut all = Vec::new();
1505        let mut cursor = None;
1506        loop {
1507            let result = self.list_resource_templates_with_cursor(cursor).await?;
1508            all.extend(result.resource_templates);
1509            match result.next_cursor {
1510                Some(c) => cursor = Some(c),
1511                None => break,
1512            }
1513        }
1514        Ok(all)
1515    }
1516
1517    /// List all prompts, following pagination cursors until exhausted.
1518    pub async fn list_all_prompts(&self) -> Result<Vec<PromptDefinition>> {
1519        let mut all = Vec::new();
1520        let mut cursor = None;
1521        loop {
1522            let result = self.list_prompts_with_cursor(cursor).await?;
1523            all.extend(result.prompts);
1524            match result.next_cursor {
1525                Some(c) => cursor = Some(c),
1526                None => break,
1527            }
1528        }
1529        Ok(all)
1530    }
1531
1532    /// Call a tool and return the concatenated text content.
1533    ///
1534    /// Returns the text from all [`Text`](crate::protocol::Content::Text) items joined together.
1535    /// If the tool result indicates an error (`is_error` is true), returns
1536    /// an error with the text content as the message.
1537    ///
1538    /// For more control over the result, use [`call_tool()`](Self::call_tool).
1539    pub async fn call_tool_text(&self, name: &str, arguments: serde_json::Value) -> Result<String> {
1540        let result = self.call_tool(name, arguments).await?;
1541        if result.is_error {
1542            return Err(Error::Internal(result.all_text()));
1543        }
1544        Ok(result.all_text())
1545    }
1546
1547    /// Get a prompt.
1548    pub async fn get_prompt(
1549        &self,
1550        name: &str,
1551        arguments: Option<std::collections::HashMap<String, String>>,
1552    ) -> Result<GetPromptResult> {
1553        let arguments = arguments.unwrap_or_default();
1554        let mut input_responses = None;
1555        let mut request_state = None;
1556        for round in 0..=self.max_mrtr_rounds {
1557            match self
1558                .get_prompt_once(
1559                    name,
1560                    arguments.clone(),
1561                    input_responses.take(),
1562                    request_state.take(),
1563                )
1564                .await?
1565            {
1566                RequestOutcome::Complete(result) => return Ok(result),
1567                RequestOutcome::InputRequired(required) => {
1568                    if round == self.max_mrtr_rounds {
1569                        return Err(Error::Transport(format!(
1570                            "MRTR round limit ({}) exceeded for prompts/get",
1571                            self.max_mrtr_rounds
1572                        )));
1573                    }
1574                    let requests = required.input_requests.ok_or_else(|| {
1575                        Error::Transport(
1576                            "input_required result has no requests the client can fulfil"
1577                                .to_string(),
1578                        )
1579                    })?;
1580                    input_responses = Some(self.resolve_input_requests(requests).await?);
1581                    request_state = required.request_state;
1582                }
1583            }
1584        }
1585        unreachable!("MRTR loop either completes or returns at the configured bound")
1586    }
1587
1588    /// Send one prompts/get attempt without automatically following MRTR.
1589    pub async fn get_prompt_once(
1590        &self,
1591        name: &str,
1592        arguments: std::collections::HashMap<String, String>,
1593        input_responses: Option<InputResponses>,
1594        request_state: Option<String>,
1595    ) -> Result<RequestOutcome<GetPromptResult>> {
1596        self.ensure_initialized()?;
1597        let params = GetPromptParams {
1598            name: name.to_string(),
1599            arguments,
1600            input_responses,
1601            request_state,
1602            meta: None,
1603        };
1604        self.send_request("prompts/get", &params).await
1605    }
1606
1607    /// Ping the server.
1608    pub async fn ping(&self) -> Result<()> {
1609        if self.uses_final_protocol().await {
1610            return Err(Error::Transport(
1611                "ping was removed from the 2026-07-28 core protocol".to_string(),
1612            ));
1613        }
1614        let _: serde_json::Value = self.send_request("ping", &serde_json::json!({})).await?;
1615        Ok(())
1616    }
1617
1618    /// Request completion suggestions from the server.
1619    pub async fn complete(
1620        &self,
1621        reference: CompletionReference,
1622        argument_name: &str,
1623        argument_value: &str,
1624    ) -> Result<CompleteResult> {
1625        self.ensure_initialized()?;
1626        let params = CompleteParams {
1627            reference,
1628            argument: CompletionArgument::new(argument_name, argument_value),
1629            context: None,
1630            meta: None,
1631        };
1632        self.send_request("completion/complete", &params).await
1633    }
1634
1635    /// Request completion for a prompt argument.
1636    pub async fn complete_prompt_arg(
1637        &self,
1638        prompt_name: &str,
1639        argument_name: &str,
1640        argument_value: &str,
1641    ) -> Result<CompleteResult> {
1642        self.complete(
1643            CompletionReference::prompt(prompt_name),
1644            argument_name,
1645            argument_value,
1646        )
1647        .await
1648    }
1649
1650    /// Request completion for a resource URI.
1651    pub async fn complete_resource_uri(
1652        &self,
1653        resource_uri: &str,
1654        argument_name: &str,
1655        argument_value: &str,
1656    ) -> Result<CompleteResult> {
1657        self.complete(
1658            CompletionReference::resource(resource_uri),
1659            argument_name,
1660            argument_value,
1661        )
1662        .await
1663    }
1664
1665    /// Send a raw typed request to the server.
1666    pub async fn request<P: serde::Serialize, R: serde::de::DeserializeOwned>(
1667        &self,
1668        method: &str,
1669        params: &P,
1670    ) -> Result<R> {
1671        self.send_request(method, params).await
1672    }
1673
1674    /// Send a raw typed notification to the server.
1675    pub async fn notify<P: serde::Serialize>(&self, method: &str, params: &P) -> Result<()> {
1676        self.send_notification(method, params).await
1677    }
1678
1679    /// Get the current roots.
1680    pub async fn roots(&self) -> Vec<Root> {
1681        self.roots.read().await.clone()
1682    }
1683
1684    /// Set roots and notify the server if initialized.
1685    pub async fn set_roots(&self, roots: Vec<Root>) -> Result<()> {
1686        *self.roots.write().await = roots;
1687        if self.is_initialized() && !self.uses_final_protocol().await {
1688            self.send_notification(notifications::ROOTS_LIST_CHANGED, &serde_json::json!({}))
1689                .await?;
1690        }
1691        Ok(())
1692    }
1693
1694    /// Add a root and notify the server if initialized.
1695    pub async fn add_root(&self, root: Root) -> Result<()> {
1696        self.roots.write().await.push(root);
1697        if self.is_initialized() && !self.uses_final_protocol().await {
1698            self.send_notification(notifications::ROOTS_LIST_CHANGED, &serde_json::json!({}))
1699                .await?;
1700        }
1701        Ok(())
1702    }
1703
1704    /// Remove a root by URI and notify the server if initialized.
1705    pub async fn remove_root(&self, uri: &str) -> Result<bool> {
1706        let mut roots = self.roots.write().await;
1707        let initial_len = roots.len();
1708        roots.retain(|r| r.uri != uri);
1709        let removed = roots.len() < initial_len;
1710        drop(roots);
1711
1712        if removed && self.is_initialized() && !self.uses_final_protocol().await {
1713            self.send_notification(notifications::ROOTS_LIST_CHANGED, &serde_json::json!({}))
1714                .await?;
1715        }
1716        Ok(removed)
1717    }
1718
1719    /// Get the roots list result (for responding to server's roots/list request).
1720    pub async fn list_roots(&self) -> ListRootsResult {
1721        ListRootsResult {
1722            roots: self.roots.read().await.clone(),
1723            meta: None,
1724        }
1725    }
1726
1727    /// Gracefully shut down the client and close the transport.
1728    pub async fn shutdown(mut self) -> Result<()> {
1729        let _ = self.command_tx.send(LoopCommand::Shutdown).await;
1730        if let Some(task) = self.task.take() {
1731            let _ = task.await;
1732        }
1733        Ok(())
1734    }
1735
1736    // --- Internal helpers ---
1737
1738    async fn send_task_aware_tool_request_with_schema_retry(
1739        &self,
1740        params: &CallToolParams,
1741        retry_available: &mut bool,
1742    ) -> Result<TaskAwareCallToolOutcome> {
1743        self.send_tool_request_with_schema_retry(params, retry_available)
1744            .await
1745    }
1746
1747    async fn wait_for_final_task(&self, task_id: &str) -> Result<crate::tasks::GetTaskResult> {
1748        loop {
1749            let task = self.task_get_detailed(task_id).await?;
1750            match task.task.status() {
1751                crate::protocol::TaskStatus::Completed
1752                | crate::protocol::TaskStatus::Failed
1753                | crate::protocol::TaskStatus::Cancelled => return Ok(task),
1754                crate::protocol::TaskStatus::InputRequired => {
1755                    let requests = task.task.input_requests().cloned().ok_or_else(|| {
1756                        Error::Transport(format!(
1757                            "task '{task_id}' is input_required without inputRequests"
1758                        ))
1759                    })?;
1760                    if requests.is_empty() {
1761                        return Err(Error::Transport(format!(
1762                            "task '{task_id}' is input_required without inputRequests"
1763                        )));
1764                    }
1765                    let responses = self.resolve_input_requests(requests).await?;
1766                    self.task_update(task_id, responses).await?;
1767                }
1768                crate::protocol::TaskStatus::Working => {
1769                    let interval_ms = task
1770                        .task
1771                        .metadata()
1772                        .poll_interval_ms
1773                        .unwrap_or(1000)
1774                        .clamp(50, 30_000);
1775                    tokio::time::sleep(std::time::Duration::from_millis(interval_ms)).await;
1776                }
1777                _ => {
1778                    return Err(Error::Transport(format!(
1779                        "task '{task_id}' returned an unsupported status"
1780                    )));
1781                }
1782            }
1783        }
1784    }
1785
1786    async fn complete_final_task(&self, task_id: &str) -> Result<CallToolResult> {
1787        let task = self.wait_for_final_task(task_id).await?;
1788        match task.task.status() {
1789            crate::protocol::TaskStatus::Completed => {
1790                let result = task.task.result().cloned().ok_or_else(|| {
1791                    Error::Transport(format!(
1792                        "completed task '{task_id}' did not contain a result"
1793                    ))
1794                })?;
1795                serde_json::from_value(serde_json::Value::Object(result)).map_err(|error| {
1796                    Error::Transport(format!(
1797                        "failed to deserialize completed task '{task_id}' result: {error}"
1798                    ))
1799                })
1800            }
1801            crate::protocol::TaskStatus::Failed => {
1802                let error = task.task.error().cloned().unwrap_or_else(|| {
1803                    JsonRpcError::internal_error(format!(
1804                        "task '{task_id}' failed without an error payload"
1805                    ))
1806                });
1807                Err(Error::JsonRpc(error))
1808            }
1809            crate::protocol::TaskStatus::Cancelled => {
1810                Err(Error::Transport(format!("task '{task_id}' was cancelled")))
1811            }
1812            _ => Err(Error::Transport(format!(
1813                "task '{task_id}' did not reach a terminal state"
1814            ))),
1815        }
1816    }
1817
1818    fn legacy_create_task_from_final(created: crate::tasks::CreateTaskResult) -> CreateTaskResult {
1819        let metadata = created.task.metadata;
1820        CreateTaskResult {
1821            task: TaskObject {
1822                task_id: metadata.task_id,
1823                status: created.task.status,
1824                status_message: metadata.status_message,
1825                created_at: metadata.created_at,
1826                last_updated_at: metadata.last_updated_at,
1827                ttl: metadata.ttl_ms,
1828                poll_interval: metadata.poll_interval_ms,
1829                result: None,
1830                error: None,
1831                meta: None,
1832            },
1833            meta: created.meta.map(serde_json::Value::Object),
1834        }
1835    }
1836
1837    fn legacy_task_from_final(result: crate::tasks::GetTaskResult) -> Result<TaskObject> {
1838        let task = &result.task;
1839        let metadata = task.metadata();
1840        let completed = task
1841            .result()
1842            .cloned()
1843            .map(serde_json::Value::Object)
1844            .map(serde_json::from_value)
1845            .transpose()
1846            .map_err(|error| {
1847                Error::Transport(format!(
1848                    "failed to deserialize task '{}' result: {error}",
1849                    metadata.task_id
1850                ))
1851            })?;
1852        Ok(TaskObject {
1853            task_id: metadata.task_id.clone(),
1854            status: task.status(),
1855            status_message: metadata.status_message.clone(),
1856            created_at: metadata.created_at.clone(),
1857            last_updated_at: metadata.last_updated_at.clone(),
1858            ttl: metadata.ttl_ms,
1859            poll_interval: metadata.poll_interval_ms,
1860            result: completed,
1861            error: task.error().cloned(),
1862            meta: result.meta.map(serde_json::Value::Object),
1863        })
1864    }
1865
1866    async fn send_tool_request_with_schema_retry<R>(
1867        &self,
1868        params: &CallToolParams,
1869        retry_available: &mut bool,
1870    ) -> Result<R>
1871    where
1872        R: serde::de::DeserializeOwned,
1873    {
1874        match self.send_request("tools/call", params).await {
1875            Err(error) if *retry_available && is_stale_tool_schema_error(&error) => {
1876                *retry_available = false;
1877                self.response_cache.evict_method("tools/list").await;
1878                tracing::info!(
1879                    tool = params.name,
1880                    error = %error,
1881                    "Refreshing tools/list before one stale-schema retry"
1882                );
1883                if let Err(refresh_error) = self.list_tools().await {
1884                    tracing::warn!(
1885                        tool = params.name,
1886                        error = %refresh_error,
1887                        "Could not refresh tools/list after stale-schema rejection"
1888                    );
1889                    return Err(error);
1890                }
1891                self.send_request("tools/call", params).await
1892            }
1893            result => result,
1894        }
1895    }
1896
1897    async fn send_cacheable_request<P, R>(
1898        &self,
1899        method: &str,
1900        cache_key: &str,
1901        params: &P,
1902    ) -> Result<R>
1903    where
1904        P: serde::Serialize,
1905        R: CacheableResponse,
1906    {
1907        let cache_allowed = self.uses_final_protocol().await;
1908        self.send_cacheable_request_when(method, cache_key, params, cache_allowed)
1909            .await
1910    }
1911
1912    async fn send_cacheable_request_when<P, R>(
1913        &self,
1914        method: &str,
1915        cache_key: &str,
1916        params: &P,
1917        cache_allowed: bool,
1918    ) -> Result<R>
1919    where
1920        P: serde::Serialize,
1921        R: CacheableResponse,
1922    {
1923        if !cache_allowed || !self.response_cache.enabled() {
1924            return self.send_request(method, params).await;
1925        }
1926
1927        let generation = self
1928            .response_cache
1929            .capture_generation(method, cache_key)
1930            .await;
1931        let mut stale = None;
1932        match self.response_cache.lookup(method, cache_key).await {
1933            CacheLookup::Fresh(value) => {
1934                if let Some(result) = decode_cached(&value, method) {
1935                    self.response_cache
1936                        .release_generation(method, cache_key)
1937                        .await;
1938                    tracing::debug!(method, "Serving fresh response from cache");
1939                    return Ok(result);
1940                }
1941                self.response_cache.evict_method(method).await;
1942            }
1943            CacheLookup::Stale(value) => stale = Some(value),
1944            CacheLookup::Miss => {}
1945        }
1946
1947        match self.send_request(method, params).await {
1948            Ok(result) => {
1949                self.write_cached_response(method, cache_key, generation, &result)
1950                    .await;
1951                Ok(result)
1952            }
1953            Err(error) => {
1954                self.response_cache
1955                    .release_generation(method, cache_key)
1956                    .await;
1957                if self.response_cache.serve_stale_on_error()
1958                    && let Some(value) = stale.as_ref()
1959                    && let Some(result) = decode_cached(value, method)
1960                {
1961                    tracing::warn!(
1962                        method,
1963                        error = %error,
1964                        "Serving stale response after cache refresh failure"
1965                    );
1966                    return Ok(result);
1967                }
1968                Err(error)
1969            }
1970        }
1971    }
1972
1973    async fn write_cached_response<R: CacheableResponse>(
1974        &self,
1975        method: &str,
1976        cache_key: &str,
1977        generation: u64,
1978        result: &R,
1979    ) {
1980        match serde_json::to_value(result) {
1981            Ok(value) => {
1982                self.response_cache
1983                    .write(
1984                        method,
1985                        cache_key,
1986                        generation,
1987                        value,
1988                        result.ttl_ms(),
1989                        result.cache_scope(),
1990                    )
1991                    .await;
1992            }
1993            Err(error) => {
1994                self.response_cache
1995                    .release_generation(method, cache_key)
1996                    .await;
1997                tracing::warn!(
1998                    method,
1999                    error = %error,
2000                    "Skipping response-cache write after serialization failure"
2001                );
2002            }
2003        }
2004    }
2005
2006    async fn send_request<P: serde::Serialize, R: serde::de::DeserializeOwned>(
2007        &self,
2008        method: &str,
2009        params: &P,
2010    ) -> Result<R> {
2011        let final_protocol = self.uses_final_protocol().await;
2012        match self.send_request_once(method, params).await {
2013            Err(Error::SessionExpired)
2014                if self.supports_session_recovery && !final_protocol && method != "initialize" =>
2015            {
2016                tracing::info!(method = %method, "Session expired, attempting recovery");
2017                self.recover_session().await?;
2018                self.send_request_once(method, params).await
2019            }
2020            other => other,
2021        }
2022    }
2023
2024    async fn send_request_once<P: serde::Serialize, R: serde::de::DeserializeOwned>(
2025        &self,
2026        method: &str,
2027        params: &P,
2028    ) -> Result<R> {
2029        self.ensure_connected()?;
2030        let params_value = serde_json::to_value(params)
2031            .map_err(|e| Error::Transport(format!("Failed to serialize params: {}", e)))?;
2032        let params_value = self.with_final_request_meta(params_value).await?;
2033        let params_value = self.with_progress_token(params_value);
2034
2035        let id = RequestId::Number(self.next_request_id.fetch_add(1, Ordering::Relaxed));
2036        let (response_tx, response_rx) = oneshot::channel();
2037        self.command_tx
2038            .send(LoopCommand::Request {
2039                id: id.clone(),
2040                method: method.to_string(),
2041                params: params_value,
2042                response_tx,
2043            })
2044            .await
2045            .map_err(|_| Error::Transport("Connection closed".to_string()))?;
2046
2047        // Dropping the returned future is what a Rust caller does to abandon
2048        // a call: a `select!` losing a race, a timeout, a Ctrl-C handler. The
2049        // server was never told, so it kept working on an answer nobody was
2050        // waiting for. This guard tells it (#1202), and disarms once the
2051        // response arrives so a completed request is never cancelled.
2052        let mut guard = CancelOnDrop {
2053            id,
2054            command_tx: self.command_tx.clone(),
2055            armed: true,
2056        };
2057
2058        let result = response_rx
2059            .await
2060            .map_err(|_| Error::Transport("Connection closed".to_string()));
2061        guard.armed = false;
2062        let result = result??;
2063
2064        serde_json::from_value(result)
2065            .map_err(|e| Error::Transport(format!("Failed to deserialize response: {}", e)))
2066    }
2067
2068    /// Recover from a session expiry by resetting the transport and re-initializing.
2069    async fn recover_session(&self) -> Result<()> {
2070        // Serialize recovery attempts
2071        let _guard = self.recovery_lock.lock().await;
2072
2073        // Check if another task already recovered while we waited
2074        // (the init_params being present means we were initialized before)
2075        let init_params = self.init_params.read().await.clone();
2076        let (client_name, client_version) = match init_params {
2077            Some(params) => params,
2078            None => {
2079                return Err(Error::Transport(
2080                    "Cannot recover: never initialized".to_string(),
2081                ));
2082            }
2083        };
2084
2085        // Tell the message loop to reset the transport
2086        let (done_tx, done_rx) = oneshot::channel();
2087        self.command_tx
2088            .send(LoopCommand::ResetSession { done_tx })
2089            .await
2090            .map_err(|_| Error::Transport("Connection closed".to_string()))?;
2091        done_rx
2092            .await
2093            .map_err(|_| Error::Transport("Connection closed during recovery".to_string()))?;
2094
2095        // Clear initialized state
2096        self.initialized.store(false, Ordering::Release);
2097        *self.server_info.write().await = None;
2098
2099        // Re-initialize (using send_request_once to avoid recursion)
2100        tracing::info!("Re-initializing session after expiry");
2101        let params = InitializeParams {
2102            protocol_version: crate::protocol::LATEST_PROTOCOL_VERSION.to_string(),
2103            capabilities: self.capabilities.clone(),
2104            client_info: Implementation {
2105                name: client_name,
2106                version: client_version,
2107                ..Default::default()
2108            },
2109            meta: None,
2110        };
2111
2112        let result: InitializeResult = self.send_request_once("initialize", &params).await?;
2113        *self.server_info.write().await = Some(result);
2114
2115        self.send_notification("notifications/initialized", &serde_json::json!({}))
2116            .await
2117            .map_err(|error| {
2118                Error::Transport(format!(
2119                    "failed to deliver notifications/initialized: {error}"
2120                ))
2121            })?;
2122        self.initialized.store(true, Ordering::Release);
2123
2124        Ok(())
2125    }
2126
2127    async fn send_notification<P: serde::Serialize>(&self, method: &str, params: &P) -> Result<()> {
2128        self.ensure_connected()?;
2129        let params_value = serde_json::to_value(params)
2130            .map_err(|e| Error::Transport(format!("Failed to serialize params: {}", e)))?;
2131        let params_value = self.with_final_request_meta(params_value).await?;
2132
2133        // Await the transport result rather than returning on enqueue: a
2134        // notification the transport failed to deliver must surface here.
2135        // `initialize()` depends on this for `notifications/initialized`;
2136        // reporting success while the handshake never completed leaves the
2137        // session unusable and every later request rejected (#1174).
2138        let (done_tx, done_rx) = oneshot::channel();
2139        self.command_tx
2140            .send(LoopCommand::Notify {
2141                method: method.to_string(),
2142                params: params_value,
2143                done_tx,
2144            })
2145            .await
2146            .map_err(|_| Error::Transport("Connection closed".to_string()))?;
2147
2148        done_rx
2149            .await
2150            .map_err(|_| Error::Transport("Connection closed".to_string()))?
2151    }
2152
2153    async fn resolve_input_requests(&self, requests: InputRequests) -> Result<InputResponses> {
2154        if !self.uses_final_protocol().await {
2155            return Err(Error::Transport(
2156                "input_required results require the 2026-07-28 client lifecycle".to_string(),
2157            ));
2158        }
2159        for request in requests.values() {
2160            let declared = match request {
2161                InputRequest::CreateMessage(_) => self.capabilities.sampling.is_some(),
2162                InputRequest::ListRoots(_) => self.capabilities.roots.is_some(),
2163                InputRequest::Elicit(_) => self.capabilities.elicitation.is_some(),
2164                _ => false,
2165            };
2166            if !declared {
2167                return Err(Error::Transport(format!(
2168                    "server requested undeclared MRTR input capability: {}",
2169                    request.method_name()
2170                )));
2171            }
2172        }
2173
2174        let (response_tx, response_rx) = oneshot::channel();
2175        self.command_tx
2176            .send(LoopCommand::ResolveInputs {
2177                requests,
2178                response_tx,
2179            })
2180            .await
2181            .map_err(|_| Error::Transport("Connection closed".to_string()))?;
2182        response_rx
2183            .await
2184            .map_err(|_| Error::Transport("Connection closed".to_string()))?
2185    }
2186
2187    async fn uses_final_protocol(&self) -> bool {
2188        self.selected_protocol_version.read().await.as_deref()
2189            == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
2190    }
2191
2192    fn request_meta_for(&self, version: &str, client_info: &Implementation) -> RequestMeta {
2193        RequestMeta {
2194            progress_token: None,
2195            protocol_version: Some(version.to_string()),
2196            client_info: Some(client_info.clone()),
2197            client_capabilities: Some(self.capabilities.clone()),
2198            log_level: None,
2199        }
2200    }
2201
2202    /// Attach a fresh progress token when the client opted into progress.
2203    ///
2204    /// Applied at the one point every typed request method funnels through,
2205    /// so `call_tool`, `read_resource`, `get_prompt`, and anything added
2206    /// later all carry a token without each growing an options argument
2207    /// (#1190). A caller that set its own token keeps it.
2208    fn with_progress_token(&self, mut params: serde_json::Value) -> serde_json::Value {
2209        let Some(tokens) = &self.progress_tokens else {
2210            return params;
2211        };
2212        // Only an object can carry `_meta`; a positional or null params value
2213        // has nowhere to put one.
2214        let Some(object) = params.as_object_mut() else {
2215            return params;
2216        };
2217        let meta = object
2218            .entry("_meta")
2219            .or_insert_with(|| serde_json::json!({}));
2220        let Some(meta) = meta.as_object_mut() else {
2221            return params;
2222        };
2223        if !meta.contains_key("progressToken") {
2224            let token = tokens.fetch_add(1, Ordering::Relaxed);
2225            meta.insert("progressToken".to_string(), serde_json::json!(token));
2226        }
2227        params
2228    }
2229
2230    async fn with_final_request_meta(
2231        &self,
2232        mut params: serde_json::Value,
2233    ) -> Result<serde_json::Value> {
2234        let Some(version) = self.selected_protocol_version.read().await.clone() else {
2235            return Ok(params);
2236        };
2237        if version != crate::protocol::PROTOCOL_VERSION_2026_07_28 {
2238            return Ok(params);
2239        }
2240        let client_info = self.client_info.read().await.clone().ok_or_else(|| {
2241            Error::Transport("final protocol selected without client identity".to_string())
2242        })?;
2243        let required = serde_json::to_value(self.request_meta_for(&version, &client_info))
2244            .map_err(|e| Error::Transport(format!("Failed to serialize request metadata: {e}")))?;
2245        let required = required
2246            .as_object()
2247            .expect("RequestMeta serializes as an object");
2248
2249        if !params.is_object() {
2250            params = serde_json::json!({});
2251        }
2252        let params_object = params
2253            .as_object_mut()
2254            .expect("params was normalized to object");
2255        let meta = params_object
2256            .entry("_meta")
2257            .or_insert_with(|| serde_json::json!({}));
2258        if !meta.is_object() {
2259            *meta = serde_json::json!({});
2260        }
2261        let meta = meta
2262            .as_object_mut()
2263            .expect("metadata was normalized to object");
2264        for (key, value) in required {
2265            meta.insert(key.clone(), value.clone());
2266        }
2267
2268        Ok(params)
2269    }
2270
2271    fn ensure_connected(&self) -> Result<()> {
2272        if !self.connected.load(Ordering::Acquire) {
2273            return Err(Error::Transport("Connection closed".to_string()));
2274        }
2275        Ok(())
2276    }
2277
2278    fn ensure_initialized(&self) -> Result<()> {
2279        if !self.initialized.load(Ordering::Acquire) {
2280            return Err(Error::Transport("Client not initialized".to_string()));
2281        }
2282        Ok(())
2283    }
2284}
2285
2286fn pagination_cache_key(cursor: Option<&str>) -> String {
2287    serde_json::to_string(&cursor).expect("pagination cursor cache key is serializable")
2288}
2289
2290fn is_stale_tool_schema_error(error: &Error) -> bool {
2291    matches!(
2292        error,
2293        Error::JsonRpc(error)
2294            if error.code == McpErrorCode::HeaderMismatch.code()
2295                || error.code == ErrorCode::MethodNotFound.code()
2296                || error.code == ErrorCode::InvalidParams.code()
2297    )
2298}
2299
2300fn decode_cached<R: serde::de::DeserializeOwned>(
2301    value: &serde_json::Value,
2302    method: &str,
2303) -> Option<R> {
2304    match serde_json::from_value(value.clone()) {
2305        Ok(result) => Some(result),
2306        Err(error) => {
2307            tracing::warn!(
2308                method,
2309                error = %error,
2310                "Discarding response-cache entry that no longer deserializes"
2311            );
2312            None
2313        }
2314    }
2315}
2316
2317impl Drop for McpClient {
2318    fn drop(&mut self) {
2319        if let Some(task) = self.task.take() {
2320            task.abort();
2321        }
2322    }
2323}
2324
2325// =============================================================================
2326// Background Message Loop
2327// =============================================================================
2328
2329/// A pending request waiting for a response from the server.
2330struct PendingRequest {
2331    method: String,
2332    response_tx: oneshot::Sender<Result<serde_json::Value>>,
2333    acknowledgment_tx: Option<oneshot::Sender<SubscriptionFilter>>,
2334}
2335
2336/// Background message loop that multiplexes incoming/outgoing messages.
2337async fn message_loop<T: ClientTransport, H: ClientHandler>(
2338    mut transport: T,
2339    handler: H,
2340    mut command_rx: mpsc::Receiver<LoopCommand>,
2341    connected: Arc<AtomicBool>,
2342    roots: Arc<RwLock<Vec<Root>>>,
2343    response_cache: Arc<ClientResponseCache>,
2344    next_id: Arc<AtomicI64>,
2345) {
2346    let handler = Arc::new(handler);
2347    let mut pending_requests: HashMap<RequestId, PendingRequest> = HashMap::new();
2348
2349    loop {
2350        tokio::select! {
2351            // Commands from McpClient methods
2352            command = command_rx.recv() => {
2353                match command {
2354                    Some(LoopCommand::Request { id, method, params, response_tx }) => {
2355
2356                        let request = JsonRpcRequest::new(id.clone(), &method)
2357                            .with_params(params);
2358                        let json = match serde_json::to_string(&request) {
2359                            Ok(j) => j,
2360                            Err(e) => {
2361                                let _ = response_tx.send(Err(Error::Transport(
2362                                    format!("Serialization failed: {}", e)
2363                                )));
2364                                continue;
2365                            }
2366                        };
2367
2368                        tracing::debug!(method = %method, id = ?id, "Sending request");
2369                        pending_requests.insert(id, PendingRequest {
2370                            method,
2371                            response_tx,
2372                            acknowledgment_tx: None,
2373                        });
2374
2375                        if let Err(e) = transport.send(&json).await {
2376                            tracing::error!(error = %e, "Transport send error");
2377                            fail_all_pending(&mut pending_requests, &format!("Transport error: {}", e));
2378                            break;
2379                        }
2380                    }
2381                    Some(LoopCommand::StartSubscription {
2382                        params,
2383                        id_tx,
2384                        acknowledgment_tx,
2385                        response_tx,
2386                    }) => {
2387                        let id = RequestId::Number(next_id.fetch_add(1, Ordering::Relaxed));
2388                        let request = JsonRpcRequest::new(id.clone(), "subscriptions/listen")
2389                            .with_params(params);
2390                        let json = match serde_json::to_string(&request) {
2391                            Ok(json) => json,
2392                            Err(error) => {
2393                                let _ = response_tx.send(Err(Error::Transport(
2394                                    format!("Serialization failed: {error}")
2395                                )));
2396                                continue;
2397                            }
2398                        };
2399
2400                        tracing::debug!(id = ?id, "Opening subscription");
2401                        pending_requests.insert(id.clone(), PendingRequest {
2402                            method: "subscriptions/listen".to_string(),
2403                            response_tx,
2404                            acknowledgment_tx: Some(acknowledgment_tx),
2405                        });
2406                        let _ = id_tx.send(id);
2407
2408                        if let Err(error) = transport.send(&json).await {
2409                            tracing::error!(%error, "Subscription transport send error");
2410                            fail_all_pending(
2411                                &mut pending_requests,
2412                                &format!("Transport error: {error}"),
2413                            );
2414                            break;
2415                        }
2416                    }
2417                    Some(LoopCommand::CancelRequest {
2418                        request_id,
2419                        done_tx,
2420                    }) => {
2421                        // Any in-flight request may be cancelled, not only a
2422                        // subscription (#1202). An id with nothing pending is
2423                        // a no-op: the response already arrived, or the caller
2424                        // cancelled twice.
2425                        let result = match pending_requests.remove(&request_id) {
2426                            Some(pending) => {
2427                                let subscription = pending.method == "subscriptions/listen";
2428                                let result = transport.cancel_request(&request_id).await;
2429                                if result.is_ok() {
2430                                    let reason = if subscription {
2431                                        "subscription cancelled"
2432                                    } else {
2433                                        "request cancelled"
2434                                    };
2435                                    let _ = pending
2436                                        .response_tx
2437                                        .send(Err(Error::Transport(reason.to_string())));
2438                                } else {
2439                                    // The notification did not go out, so the
2440                                    // request is still live on the server;
2441                                    // put it back rather than orphan it.
2442                                    pending_requests.insert(request_id.clone(), pending);
2443                                }
2444                                result
2445                            }
2446                            None => Ok(()),
2447                        };
2448                        if let Some(done_tx) = done_tx {
2449                            let _ = done_tx.send(result);
2450                        }
2451                    }
2452                    Some(LoopCommand::Notify { method, params, done_tx }) => {
2453                        let notification = JsonRpcNotification::new(&method)
2454                            .with_params(params);
2455                        let result = match serde_json::to_string(&notification) {
2456                            Ok(json) => {
2457                                tracing::debug!(method = %method, "Sending notification");
2458                                transport.send(&json).await
2459                            }
2460                            Err(error) => Err(Error::Transport(format!(
2461                                "Failed to serialize notification: {error}"
2462                            ))),
2463                        };
2464                        if let Err(error) = &result {
2465                            tracing::warn!(method = %method, %error, "Notification send failed");
2466                        }
2467                        let _ = done_tx.send(result);
2468                    }
2469                    Some(LoopCommand::ResolveInputs { requests, response_tx }) => {
2470                        let result = resolve_inputs_with_handler(&handler, &roots, requests).await;
2471                        let _ = response_tx.send(result);
2472                    }
2473                    Some(LoopCommand::ResetSession { done_tx }) => {
2474                        tracing::info!("Resetting transport session for re-initialization");
2475                        transport.reset_session().await;
2476                        // Fail any pending requests with session expired
2477                        for (_, pending) in pending_requests.drain() {
2478                            let _ = pending.response_tx.send(Err(Error::SessionExpired));
2479                        }
2480                        let _ = done_tx.send(());
2481                    }
2482                    Some(LoopCommand::Shutdown) | None => {
2483                        tracing::debug!("Message loop shutting down");
2484                        break;
2485                    }
2486                }
2487            }
2488
2489            // Incoming messages from the server
2490            result = transport.recv() => {
2491                match result {
2492                    Ok(Some(line)) => {
2493                        handle_incoming(
2494                            &line,
2495                            &mut pending_requests,
2496                            &handler,
2497                            &roots,
2498                            &mut transport,
2499                            &response_cache,
2500                        ).await;
2501                    }
2502                    Ok(None) => {
2503                        tracing::info!("Transport closed (EOF)");
2504                        break;
2505                    }
2506                    Err(e) => {
2507                        tracing::error!(error = %e, "Transport receive error");
2508                        break;
2509                    }
2510                }
2511            }
2512        }
2513    }
2514
2515    // Cleanup
2516    connected.store(false, Ordering::Release);
2517    fail_all_pending(&mut pending_requests, "Connection closed");
2518    let _ = transport.close().await;
2519}
2520
2521async fn resolve_inputs_with_handler<H: ClientHandler>(
2522    handler: &Arc<H>,
2523    roots: &Arc<RwLock<Vec<Root>>>,
2524    requests: InputRequests,
2525) -> Result<InputResponses> {
2526    let mut responses = InputResponses::new();
2527    for (key, request) in requests {
2528        let response = match request {
2529            InputRequest::CreateMessage(params) => InputResponse::CreateMessage(
2530                handler
2531                    .handle_create_message(params)
2532                    .await
2533                    .map_err(Error::JsonRpc)?,
2534            ),
2535            InputRequest::ListRoots(_) => {
2536                let configured = roots.read().await.clone();
2537                let result = if configured.is_empty() {
2538                    handler.handle_list_roots().await.map_err(Error::JsonRpc)?
2539                } else {
2540                    ListRootsResult {
2541                        roots: configured,
2542                        meta: None,
2543                    }
2544                };
2545                InputResponse::ListRoots(result)
2546            }
2547            InputRequest::Elicit(params) => InputResponse::Elicit(
2548                handler
2549                    .handle_elicit(params)
2550                    .await
2551                    .map_err(Error::JsonRpc)?,
2552            ),
2553            _ => {
2554                return Err(Error::Transport(
2555                    "unsupported MRTR input request method".to_string(),
2556                ));
2557            }
2558        };
2559        responses.insert(key, response);
2560    }
2561    Ok(responses)
2562}
2563
2564/// Handle a single incoming message from the server.
2565async fn handle_incoming<T: ClientTransport, H: ClientHandler>(
2566    line: &str,
2567    pending_requests: &mut HashMap<RequestId, PendingRequest>,
2568    handler: &Arc<H>,
2569    roots: &Arc<RwLock<Vec<Root>>>,
2570    transport: &mut T,
2571    response_cache: &Arc<ClientResponseCache>,
2572) {
2573    let parsed: serde_json::Value = match serde_json::from_str(line) {
2574        Ok(v) => v,
2575        Err(e) => {
2576            tracing::warn!(error = %e, "Failed to parse incoming message");
2577            return;
2578        }
2579    };
2580
2581    // Case 1: Response to one of our pending requests (has result or error, no method)
2582    if parsed.get("method").is_none()
2583        && (parsed.get("result").is_some() || parsed.get("error").is_some())
2584    {
2585        // Check for session-level errors (id: null with -32005) that affect
2586        // all pending requests, not just a specific one.
2587        if let Some(error) = parsed.get("error") {
2588            let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(0) as i32;
2589            let id_missing_or_null = parsed.get("id").is_none_or(|id| id.is_null());
2590            if code == -32005 && id_missing_or_null {
2591                tracing::warn!(
2592                    "Session expired (-32005 with null id), failing all pending requests"
2593                );
2594                for (_, pending) in pending_requests.drain() {
2595                    let _ = pending.response_tx.send(Err(Error::SessionExpired));
2596                }
2597                return;
2598            }
2599        }
2600
2601        handle_response(&parsed, pending_requests);
2602        return;
2603    }
2604
2605    // Case 2: Server-initiated request (has id + method)
2606    if parsed.get("id").is_some() && parsed.get("method").is_some() {
2607        let id = parse_request_id(&parsed);
2608        let method = parsed["method"].as_str().unwrap_or("");
2609        let params = parsed.get("params").cloned();
2610
2611        let result = dispatch_server_request(handler, roots, method, params).await;
2612
2613        // Send response back to the server
2614        let response = match result {
2615            Ok(value) => {
2616                if let Some(id) = id {
2617                    serde_json::json!({
2618                        "jsonrpc": "2.0",
2619                        "id": id,
2620                        "result": value
2621                    })
2622                } else {
2623                    return;
2624                }
2625            }
2626            Err(error) => {
2627                serde_json::json!({
2628                    "jsonrpc": "2.0",
2629                    "id": id,
2630                    "error": {
2631                        "code": error.code,
2632                        "message": error.message
2633                    }
2634                })
2635            }
2636        };
2637
2638        if let Ok(json) = serde_json::to_string(&response) {
2639            let _ = transport.send(&json).await;
2640        }
2641        return;
2642    }
2643
2644    // Case 3: Server notification (has method, no id)
2645    if parsed.get("method").is_some() && parsed.get("id").is_none() {
2646        let method = parsed["method"].as_str().unwrap_or("");
2647        let params = parsed.get("params").cloned();
2648        invalidate_response_cache(response_cache, method, params.as_ref()).await;
2649        let notification = parse_server_notification(method, params);
2650        let should_dispatch = match &notification {
2651            ServerNotification::SubscriptionAcknowledged {
2652                subscription_id,
2653                notifications,
2654            } => {
2655                let Some(key) = matching_subscription_id(pending_requests, subscription_id) else {
2656                    tracing::warn!(
2657                        id = ?subscription_id,
2658                        "Ignoring acknowledgment for unknown subscription"
2659                    );
2660                    return;
2661                };
2662                if let Some(sender) = pending_requests
2663                    .get_mut(&key)
2664                    .and_then(|pending| pending.acknowledgment_tx.take())
2665                {
2666                    let _ = sender.send(notifications.clone());
2667                    true
2668                } else {
2669                    tracing::warn!(
2670                        id = ?subscription_id,
2671                        "Ignoring duplicate subscription acknowledgment"
2672                    );
2673                    false
2674                }
2675            }
2676            ServerNotification::Subscription {
2677                subscription_id, ..
2678            } => {
2679                let Some(key) = matching_subscription_id(pending_requests, subscription_id) else {
2680                    tracing::warn!(
2681                        id = ?subscription_id,
2682                        "Ignoring notification for unknown subscription"
2683                    );
2684                    return;
2685                };
2686                let before_acknowledgment = pending_requests
2687                    .get(&key)
2688                    .is_some_and(|pending| pending.acknowledgment_tx.is_some());
2689                if before_acknowledgment {
2690                    tracing::warn!(
2691                        id = ?subscription_id,
2692                        "Ending subscription that received a notification before acknowledgment"
2693                    );
2694                    if let Some(pending) = pending_requests.remove(&key) {
2695                        let _ = pending.response_tx.send(Err(Error::Transport(
2696                            "subscription notification arrived before acknowledgment".to_string(),
2697                        )));
2698                    }
2699                    false
2700                } else {
2701                    true
2702                }
2703            }
2704            ServerNotification::SubscriptionCancelled {
2705                subscription_id,
2706                reason,
2707            } => {
2708                let Some(key) = matching_subscription_id(pending_requests, subscription_id) else {
2709                    tracing::warn!(
2710                        id = ?subscription_id,
2711                        "Ignoring cancellation for unknown or non-subscription request"
2712                    );
2713                    return;
2714                };
2715                if let Some(pending) = pending_requests.remove(&key) {
2716                    let message = reason.as_deref().map_or_else(
2717                        || "subscription cancelled by server".to_string(),
2718                        |reason| format!("subscription cancelled by server: {reason}"),
2719                    );
2720                    let _ = pending.response_tx.send(Err(Error::Transport(message)));
2721                }
2722                true
2723            }
2724            _ => true,
2725        };
2726        if should_dispatch {
2727            handler.on_notification(notification).await;
2728        }
2729    }
2730}
2731
2732fn matching_subscription_id(
2733    pending_requests: &HashMap<RequestId, PendingRequest>,
2734    id: &RequestId,
2735) -> Option<RequestId> {
2736    if pending_requests
2737        .get(id)
2738        .is_some_and(|pending| pending.method == "subscriptions/listen")
2739    {
2740        return Some(id.clone());
2741    }
2742    pending_requests.iter().find_map(|(candidate, pending)| {
2743        (pending.method == "subscriptions/listen" && request_ids_match(candidate, id))
2744            .then(|| candidate.clone())
2745    })
2746}
2747
2748fn request_ids_match(left: &RequestId, right: &RequestId) -> bool {
2749    left == right
2750        || matches!(
2751            (left, right),
2752            (RequestId::Number(number), RequestId::String(value))
2753                | (RequestId::String(value), RequestId::Number(number))
2754                if value.parse::<i64>() == Ok(*number)
2755        )
2756}
2757
2758async fn invalidate_response_cache(
2759    response_cache: &ClientResponseCache,
2760    method: &str,
2761    params: Option<&serde_json::Value>,
2762) {
2763    match method {
2764        notifications::TOOLS_LIST_CHANGED => {
2765            response_cache.evict_method("tools/list").await;
2766        }
2767        notifications::PROMPTS_LIST_CHANGED => {
2768            response_cache.evict_method("prompts/list").await;
2769        }
2770        notifications::RESOURCES_LIST_CHANGED => {
2771            response_cache.evict_method("resources/list").await;
2772            response_cache
2773                .evict_method("resources/templates/list")
2774                .await;
2775        }
2776        notifications::RESOURCE_UPDATED => {
2777            if let Some(uri) = params
2778                .and_then(|value| value.get("uri"))
2779                .and_then(serde_json::Value::as_str)
2780            {
2781                response_cache.evict_resource(uri).await;
2782            }
2783        }
2784        _ => {}
2785    }
2786}
2787
2788/// Handle a JSON-RPC response by routing to the pending request.
2789fn handle_response(
2790    parsed: &serde_json::Value,
2791    pending_requests: &mut HashMap<RequestId, PendingRequest>,
2792) {
2793    let id = match parse_request_id(parsed) {
2794        Some(id) => id,
2795        None => {
2796            tracing::warn!("Response without id");
2797            return;
2798        }
2799    };
2800
2801    // Exact match first; genuine string IDs always take precedence. As a
2802    // fallback, accept a response whose id is the string form of a numeric
2803    // request id ("42" matching 42) -- some servers stringify numeric ids
2804    // when echoing them (rmcp #1021 analog).
2805    let pending = match pending_requests.remove(&id) {
2806        Some(p) => p,
2807        None => {
2808            let numeric_fallback = match &id {
2809                RequestId::String(s) => s
2810                    .parse::<i64>()
2811                    .ok()
2812                    .and_then(|n| pending_requests.remove(&RequestId::Number(n))),
2813                _ => None,
2814            };
2815            match numeric_fallback {
2816                Some(p) => p,
2817                None => {
2818                    tracing::warn!(id = ?id, "Response for unknown request");
2819                    return;
2820                }
2821            }
2822        }
2823    };
2824
2825    tracing::debug!(id = ?id, "Received response");
2826
2827    if let Some(error) = parsed.get("error") {
2828        let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
2829        let message = error
2830            .get("message")
2831            .and_then(|m| m.as_str())
2832            .unwrap_or("Unknown error")
2833            .to_string();
2834        let data = error.get("data").cloned();
2835
2836        // -32005 = SessionNotFound: signal session expiry for recovery
2837        if code == -32005 {
2838            let _ = pending.response_tx.send(Err(Error::SessionExpired));
2839            return;
2840        }
2841
2842        let json_rpc_error = JsonRpcError {
2843            code,
2844            message,
2845            data,
2846        };
2847        let _ = pending
2848            .response_tx
2849            .send(Err(Error::JsonRpc(json_rpc_error)));
2850    } else if let Some(result) = parsed.get("result") {
2851        if pending.method == "subscriptions/listen" && pending.acknowledgment_tx.is_some() {
2852            let _ = pending.response_tx.send(Err(Error::Transport(
2853                "subscriptions/listen completed before acknowledgment".to_string(),
2854            )));
2855            return;
2856        }
2857        let _ = pending.response_tx.send(Ok(result.clone()));
2858    } else {
2859        let _ = pending
2860            .response_tx
2861            .send(Err(Error::Transport("Invalid response".to_string())));
2862    }
2863}
2864
2865/// Dispatch a server-initiated request to the handler.
2866async fn dispatch_server_request<H: ClientHandler>(
2867    handler: &Arc<H>,
2868    roots: &Arc<RwLock<Vec<Root>>>,
2869    method: &str,
2870    params: Option<serde_json::Value>,
2871) -> std::result::Result<serde_json::Value, JsonRpcError> {
2872    match method {
2873        "sampling/createMessage" => {
2874            let p = serde_json::from_value(params.unwrap_or_default())
2875                .map_err(|e| JsonRpcError::invalid_params(e.to_string()))?;
2876            let result = handler.handle_create_message(p).await?;
2877            serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
2878        }
2879        "elicitation/create" => {
2880            let p = serde_json::from_value(params.unwrap_or_default())
2881                .map_err(|e| JsonRpcError::invalid_params(e.to_string()))?;
2882            let result = handler.handle_elicit(p).await?;
2883            serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
2884        }
2885        "roots/list" => {
2886            // Use client-configured roots if available, otherwise delegate to handler
2887            let roots_list = roots.read().await;
2888            if !roots_list.is_empty() {
2889                let result = ListRootsResult {
2890                    roots: roots_list.clone(),
2891                    meta: None,
2892                };
2893                return serde_json::to_value(result)
2894                    .map_err(|e| JsonRpcError::internal_error(e.to_string()));
2895            }
2896            drop(roots_list);
2897
2898            let result = handler.handle_list_roots().await?;
2899            serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
2900        }
2901        "ping" => Ok(serde_json::json!({})),
2902        _ => Err(JsonRpcError::method_not_found(method)),
2903    }
2904}
2905
2906/// Parse a request ID from a JSON-RPC message.
2907fn parse_request_id(parsed: &serde_json::Value) -> Option<RequestId> {
2908    parsed.get("id").and_then(|id| {
2909        if let Some(n) = id.as_i64() {
2910            Some(RequestId::Number(n))
2911        } else {
2912            id.as_str().map(|s| RequestId::String(s.to_string()))
2913        }
2914    })
2915}
2916
2917/// Parse a server notification into the typed enum.
2918fn parse_server_notification(
2919    method: &str,
2920    params: Option<serde_json::Value>,
2921) -> ServerNotification {
2922    if method == notifications::SUBSCRIPTIONS_ACKNOWLEDGED {
2923        if let Some(params) = &params
2924            && let Ok(acknowledgment) =
2925                serde_json::from_value::<SubscriptionsAcknowledgedParams>(params.clone())
2926            && let Some(subscription_id) = acknowledgment.meta.and_then(|meta| meta.subscription_id)
2927        {
2928            return ServerNotification::SubscriptionAcknowledged {
2929                subscription_id,
2930                notifications: acknowledgment.notifications,
2931            };
2932        }
2933        return ServerNotification::Unknown {
2934            method: method.to_string(),
2935            params,
2936        };
2937    }
2938    if method == notifications::CANCELLED {
2939        if let Some(params) = &params
2940            && let Ok(cancelled) = serde_json::from_value::<CancelledParams>(params.clone())
2941            && let Some(subscription_id) = cancelled.request_id
2942        {
2943            return ServerNotification::SubscriptionCancelled {
2944                subscription_id,
2945                reason: cancelled.reason,
2946            };
2947        }
2948        return ServerNotification::Unknown {
2949            method: method.to_string(),
2950            params,
2951        };
2952    }
2953
2954    let subscription_id = params
2955        .as_ref()
2956        .and_then(|params| params.pointer("/_meta/io.modelcontextprotocol~1subscriptionId"))
2957        .and_then(|id| serde_json::from_value::<RequestId>(id.clone()).ok());
2958    let notification = match method {
2959        notifications::PROGRESS => {
2960            if let Some(params) = params.clone()
2961                && let Ok(p) = serde_json::from_value(params)
2962            {
2963                ServerNotification::Progress(p)
2964            } else {
2965                ServerNotification::Unknown {
2966                    method: method.to_string(),
2967                    params: None,
2968                }
2969            }
2970        }
2971        notifications::MESSAGE => {
2972            if let Some(params) = params.clone()
2973                && let Ok(p) = serde_json::from_value(params)
2974            {
2975                ServerNotification::LogMessage(p)
2976            } else {
2977                ServerNotification::Unknown {
2978                    method: method.to_string(),
2979                    params: None,
2980                }
2981            }
2982        }
2983        notifications::RESOURCE_UPDATED => {
2984            if let Some(params) = &params
2985                && let Some(uri) = params.get("uri").and_then(|u| u.as_str())
2986            {
2987                ServerNotification::ResourceUpdated {
2988                    uri: uri.to_string(),
2989                }
2990            } else {
2991                ServerNotification::Unknown {
2992                    method: method.to_string(),
2993                    params: params.clone(),
2994                }
2995            }
2996        }
2997        notifications::RESOURCES_LIST_CHANGED => ServerNotification::ResourcesListChanged,
2998        notifications::TOOLS_LIST_CHANGED => ServerNotification::ToolsListChanged,
2999        notifications::PROMPTS_LIST_CHANGED => ServerNotification::PromptsListChanged,
3000        notifications::TASK_STATUS_CHANGED => {
3001            let is_final = params
3002                .as_ref()
3003                .and_then(serde_json::Value::as_object)
3004                .is_some_and(|params| params.contains_key("ttlMs"));
3005            match (is_final, params.clone()) {
3006                (true, Some(params)) => {
3007                    match serde_json::from_value::<crate::tasks::TaskStatusNotificationParams>(
3008                        params.clone(),
3009                    ) {
3010                        Ok(params) => ServerNotification::FinalTaskStatusChanged(params),
3011                        Err(_) => ServerNotification::Unknown {
3012                            method: method.to_string(),
3013                            params: Some(params),
3014                        },
3015                    }
3016                }
3017                (false, Some(params)) => {
3018                    match serde_json::from_value::<TaskStatusParams>(params.clone()) {
3019                        Ok(params) => ServerNotification::TaskStatusChanged(params),
3020                        Err(_) => ServerNotification::Unknown {
3021                            method: method.to_string(),
3022                            params: Some(params),
3023                        },
3024                    }
3025                }
3026                (_, None) => ServerNotification::Unknown {
3027                    method: method.to_string(),
3028                    params: None,
3029                },
3030            }
3031        }
3032        _ => ServerNotification::Unknown {
3033            method: method.to_string(),
3034            params: params.clone(),
3035        },
3036    };
3037    if let Some(subscription_id) = subscription_id {
3038        ServerNotification::Subscription {
3039            subscription_id,
3040            notification: Box::new(notification),
3041        }
3042    } else {
3043        notification
3044    }
3045}
3046
3047/// Fail all pending requests with the given error message.
3048fn fail_all_pending(pending: &mut HashMap<RequestId, PendingRequest>, reason: &str) {
3049    for (_, req) in pending.drain() {
3050        let _ = req
3051            .response_tx
3052            .send(Err(Error::Transport(reason.to_string())));
3053    }
3054}
3055
3056#[cfg(test)]
3057mod tests {
3058    use super::*;
3059    use async_trait::async_trait;
3060    use std::sync::Mutex;
3061
3062    /// Mock transport for testing that auto-responds to requests.
3063    ///
3064    /// When the client sends a request via `send()`, the mock extracts the
3065    /// request ID, pairs it with the next preconfigured response, and feeds
3066    /// it back through a channel that `recv()` awaits on. This ensures
3067    /// `recv()` blocks when no messages are available (instead of returning
3068    /// EOF), keeping the background message loop alive.
3069    struct MockTransport {
3070        /// Pre-configured result or error replies (not full envelopes).
3071        responses: Arc<Mutex<Vec<MockReply>>>,
3072        /// Index of the next response to use.
3073        response_idx: Arc<std::sync::atomic::AtomicUsize>,
3074        /// Channel sender for feeding responses back to `recv()`.
3075        incoming_tx: mpsc::Sender<String>,
3076        /// Channel receiver for `recv()` to await on.
3077        incoming_rx: mpsc::Receiver<String>,
3078        /// Collected outgoing messages from `send()`.
3079        outgoing: Arc<Mutex<Vec<String>>>,
3080        connected: Arc<AtomicBool>,
3081        /// When set, `send()` fails for notifications (messages without an
3082        /// `id`), simulating a transport that could not deliver them.
3083        fail_notification_sends: Arc<AtomicBool>,
3084    }
3085
3086    enum MockReply {
3087        Result(serde_json::Value),
3088        Error(JsonRpcError),
3089    }
3090
3091    #[allow(dead_code)]
3092    impl MockTransport {
3093        fn new() -> Self {
3094            let (tx, rx) = mpsc::channel(32);
3095            Self {
3096                responses: Arc::new(Mutex::new(Vec::new())),
3097                response_idx: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
3098                incoming_tx: tx,
3099                incoming_rx: rx,
3100                outgoing: Arc::new(Mutex::new(Vec::new())),
3101                connected: Arc::new(AtomicBool::new(true)),
3102                fail_notification_sends: Arc::new(AtomicBool::new(false)),
3103            }
3104        }
3105
3106        /// Create a mock that auto-responds with the given result payloads.
3107        ///
3108        /// When `send()` receives a JSON-RPC request, it extracts the request
3109        /// ID and pairs it with the next response from this list, sending the
3110        /// complete JSON-RPC response through the channel for `recv()`.
3111        fn with_responses(responses: Vec<serde_json::Value>) -> Self {
3112            let (tx, rx) = mpsc::channel(32);
3113            Self {
3114                responses: Arc::new(Mutex::new(
3115                    responses.into_iter().map(MockReply::Result).collect(),
3116                )),
3117                response_idx: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
3118                incoming_tx: tx,
3119                incoming_rx: rx,
3120                outgoing: Arc::new(Mutex::new(Vec::new())),
3121                connected: Arc::new(AtomicBool::new(true)),
3122                fail_notification_sends: Arc::new(AtomicBool::new(false)),
3123            }
3124        }
3125
3126        fn with_replies(responses: Vec<MockReply>) -> Self {
3127            let (tx, rx) = mpsc::channel(32);
3128            Self {
3129                responses: Arc::new(Mutex::new(responses)),
3130                response_idx: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
3131                incoming_tx: tx,
3132                incoming_rx: rx,
3133                outgoing: Arc::new(Mutex::new(Vec::new())),
3134                connected: Arc::new(AtomicBool::new(true)),
3135                fail_notification_sends: Arc::new(AtomicBool::new(false)),
3136            }
3137        }
3138    }
3139
3140    #[async_trait]
3141    impl ClientTransport for MockTransport {
3142        async fn send(&mut self, message: &str) -> Result<()> {
3143            self.outgoing.lock().unwrap().push(message.to_string());
3144
3145            // Parse the outgoing message to extract the request ID
3146            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(message) {
3147                if parsed.get("id").is_none()
3148                    && self.fail_notification_sends.load(Ordering::Relaxed)
3149                {
3150                    return Err(Error::Transport(
3151                        "mock transport dropped the notification".to_string(),
3152                    ));
3153                }
3154                // Only respond to requests (messages with an id and method)
3155                if let Some(id) = parsed.get("id") {
3156                    let idx = self
3157                        .response_idx
3158                        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3159                    let responses = self.responses.lock().unwrap();
3160                    if let Some(reply) = responses.get(idx) {
3161                        let response = match reply {
3162                            MockReply::Result(result) => serde_json::json!({
3163                                "jsonrpc": "2.0",
3164                                "id": id,
3165                                "result": result
3166                            }),
3167                            MockReply::Error(error) => serde_json::json!({
3168                                "jsonrpc": "2.0",
3169                                "id": id,
3170                                "error": error
3171                            }),
3172                        };
3173                        let _ = self.incoming_tx.try_send(response.to_string());
3174                    }
3175                }
3176            }
3177
3178            Ok(())
3179        }
3180
3181        async fn recv(&mut self) -> Result<Option<String>> {
3182            // Await on the channel -- blocks until a message is available
3183            // or the sender is dropped (returns None = EOF).
3184            match self.incoming_rx.recv().await {
3185                Some(msg) => Ok(Some(msg)),
3186                None => Ok(None),
3187            }
3188        }
3189
3190        fn is_connected(&self) -> bool {
3191            self.connected.load(Ordering::Relaxed)
3192        }
3193
3194        async fn close(&mut self) -> Result<()> {
3195            self.connected.store(false, Ordering::Relaxed);
3196            Ok(())
3197        }
3198    }
3199
3200    fn mock_initialize_response() -> serde_json::Value {
3201        serde_json::json!({
3202            "protocolVersion": "2025-11-25",
3203            "serverInfo": {
3204                "name": "test-server",
3205                "version": "1.0.0"
3206            },
3207            "capabilities": {
3208                "tools": {}
3209            }
3210        })
3211    }
3212
3213    /// #1174: a notification the transport fails to deliver must fail
3214    /// `initialize()`. Reporting success left the handshake incomplete and a
3215    /// strict server rejecting every subsequent request with -32600.
3216    #[tokio::test]
3217    async fn initialize_fails_when_initialized_notification_is_not_delivered() {
3218        let transport = MockTransport::with_responses(vec![mock_initialize_response()]);
3219        let fail_notifications = transport.fail_notification_sends.clone();
3220        let outgoing = transport.outgoing.clone();
3221        // The initialize request itself succeeds; only the follow-up
3222        // notification is dropped.
3223        fail_notifications.store(true, Ordering::Relaxed);
3224
3225        let client = McpClient::connect(transport).await.unwrap();
3226        let error = client
3227            .initialize("test-client", "1.0.0")
3228            .await
3229            .expect_err("undelivered notifications/initialized must fail initialize");
3230
3231        assert!(
3232            error
3233                .to_string()
3234                .contains("failed to deliver notifications/initialized"),
3235            "error should name the handshake step, got: {error}"
3236        );
3237        // The notification was attempted, not skipped.
3238        assert!(
3239            outgoing
3240                .lock()
3241                .unwrap()
3242                .iter()
3243                .any(|message| message.contains("notifications/initialized")),
3244            "the client must have tried to send the notification"
3245        );
3246        // The client must not consider itself initialized.
3247        assert!(!client.is_initialized());
3248    }
3249
3250    #[tokio::test]
3251    async fn notification_delivery_errors_reach_the_caller() {
3252        let transport = MockTransport::with_responses(vec![mock_initialize_response()]);
3253        let fail_notifications = transport.fail_notification_sends.clone();
3254
3255        let client = McpClient::connect(transport).await.unwrap();
3256        client.initialize("test-client", "1.0.0").await.unwrap();
3257
3258        // Healthy so far; now the transport starts dropping notifications.
3259        fail_notifications.store(true, Ordering::Relaxed);
3260        let error = client
3261            .notify("notifications/progress", &serde_json::json!({}))
3262            .await
3263            .expect_err("a dropped notification must surface as an error");
3264        assert!(error.to_string().contains("dropped the notification"));
3265    }
3266
3267    #[tokio::test]
3268    async fn test_client_not_initialized() {
3269        let client = McpClient::connect(MockTransport::with_responses(vec![]))
3270            .await
3271            .unwrap();
3272
3273        let result = client.list_tools().await;
3274        assert!(result.is_err());
3275        assert!(result.unwrap_err().to_string().contains("not initialized"));
3276    }
3277
3278    #[tokio::test]
3279    async fn test_client_initialize() {
3280        let client = McpClient::connect(MockTransport::with_responses(vec![
3281            mock_initialize_response(),
3282        ]))
3283        .await
3284        .unwrap();
3285
3286        assert!(!client.is_initialized());
3287
3288        let result = client.initialize("test-client", "1.0.0").await;
3289        assert!(result.is_ok());
3290        assert!(client.is_initialized());
3291
3292        let server_info = client.server_info().await.unwrap();
3293        assert_eq!(server_info.server_info.name, "test-server");
3294    }
3295
3296    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3297    #[tokio::test]
3298    async fn final_discover_injects_metadata_on_every_request() {
3299        let transport = MockTransport::with_responses(vec![
3300            serde_json::json!({
3301                "resultType": "complete",
3302                "supportedVersions": ["2026-07-28"],
3303                "capabilities": {}
3304            }),
3305            serde_json::json!({
3306                "resultType": "complete",
3307                "tools": [],
3308                "ttlMs": 0,
3309                "cacheScope": "private"
3310            }),
3311        ]);
3312        let outgoing = transport.outgoing.clone();
3313        let client = McpClient::builder()
3314            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3315            .with_elicitation()
3316            .connect_simple(transport)
3317            .await
3318            .unwrap();
3319
3320        client.discover("test-client", "1.0.0").await.unwrap();
3321        client.list_tools().await.unwrap();
3322        assert_eq!(
3323            client.selected_protocol_version().await.as_deref(),
3324            Some("2026-07-28")
3325        );
3326
3327        let messages: Vec<serde_json::Value> = outgoing
3328            .lock()
3329            .unwrap()
3330            .iter()
3331            .map(|message| serde_json::from_str(message).unwrap())
3332            .collect();
3333        assert_eq!(messages.len(), 2);
3334        assert_eq!(messages[0]["method"], "server/discover");
3335        assert_eq!(messages[1]["method"], "tools/list");
3336        for message in messages {
3337            let meta = &message["params"]["_meta"];
3338            assert_eq!(
3339                meta["io.modelcontextprotocol/protocolVersion"],
3340                "2026-07-28"
3341            );
3342            assert_eq!(
3343                meta["io.modelcontextprotocol/clientInfo"]["name"],
3344                "test-client"
3345            );
3346            assert!(meta["io.modelcontextprotocol/clientCapabilities"].is_object());
3347        }
3348    }
3349
3350    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3351    fn final_discover_result() -> serde_json::Value {
3352        serde_json::json!({
3353            "resultType": "complete",
3354            "supportedVersions": ["2026-07-28"],
3355            "capabilities": {},
3356            "ttlMs": 0,
3357            "cacheScope": "private"
3358        })
3359    }
3360
3361    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3362    struct SubscriptionTestTransport {
3363        incoming_tx: mpsc::Sender<String>,
3364        incoming_rx: mpsc::Receiver<String>,
3365        outgoing: Arc<Mutex<Vec<String>>>,
3366        connected: Arc<AtomicBool>,
3367    }
3368
3369    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3370    impl SubscriptionTestTransport {
3371        fn new() -> Self {
3372            let (incoming_tx, incoming_rx) = mpsc::channel(32);
3373            Self {
3374                incoming_tx,
3375                incoming_rx,
3376                outgoing: Arc::new(Mutex::new(Vec::new())),
3377                connected: Arc::new(AtomicBool::new(true)),
3378            }
3379        }
3380    }
3381
3382    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3383    #[async_trait]
3384    impl ClientTransport for SubscriptionTestTransport {
3385        async fn send(&mut self, message: &str) -> Result<()> {
3386            self.outgoing.lock().unwrap().push(message.to_string());
3387            let value: serde_json::Value = serde_json::from_str(message)
3388                .map_err(|error| Error::Transport(error.to_string()))?;
3389            let Some(id) = value.get("id").cloned() else {
3390                return Ok(());
3391            };
3392            match value
3393                .get("method")
3394                .and_then(serde_json::Value::as_str)
3395                .unwrap_or_default()
3396            {
3397                "server/discover" => {
3398                    self.incoming_tx
3399                        .send(
3400                            serde_json::json!({
3401                                "jsonrpc": "2.0",
3402                                "id": id,
3403                                "result": final_discover_result()
3404                            })
3405                            .to_string(),
3406                        )
3407                        .await
3408                        .map_err(|_| Error::Transport("test channel closed".to_string()))?;
3409                }
3410                "subscriptions/listen" => {
3411                    let notifications = value["params"]["notifications"].clone();
3412                    self.incoming_tx
3413                        .send(
3414                            serde_json::json!({
3415                                "jsonrpc": "2.0",
3416                                "method": notifications::SUBSCRIPTIONS_ACKNOWLEDGED,
3417                                "params": {
3418                                    "_meta": {
3419                                        "io.modelcontextprotocol/subscriptionId": id
3420                                    },
3421                                    "notifications": notifications
3422                                }
3423                            })
3424                            .to_string(),
3425                        )
3426                        .await
3427                        .map_err(|_| Error::Transport("test channel closed".to_string()))?;
3428
3429                    if value["params"]["notifications"]["promptsListChanged"]
3430                        == serde_json::Value::Bool(true)
3431                    {
3432                        self.incoming_tx
3433                            .send(
3434                                serde_json::json!({
3435                                    "jsonrpc": "2.0",
3436                                    "id": id,
3437                                    "result": {
3438                                        "resultType": "complete",
3439                                        "_meta": {
3440                                            "io.modelcontextprotocol/subscriptionId": id
3441                                        }
3442                                    }
3443                                })
3444                                .to_string(),
3445                            )
3446                            .await
3447                            .map_err(|_| Error::Transport("test channel closed".to_string()))?;
3448                    } else {
3449                        self.incoming_tx
3450                            .send(
3451                                serde_json::json!({
3452                                    "jsonrpc": "2.0",
3453                                    "method": notifications::TOOLS_LIST_CHANGED,
3454                                    "params": {
3455                                        "_meta": {
3456                                            "io.modelcontextprotocol/subscriptionId": id
3457                                        }
3458                                    }
3459                                })
3460                                .to_string(),
3461                            )
3462                            .await
3463                            .map_err(|_| Error::Transport("test channel closed".to_string()))?;
3464                    }
3465                }
3466                _ => {}
3467            }
3468            Ok(())
3469        }
3470
3471        async fn recv(&mut self) -> Result<Option<String>> {
3472            Ok(self.incoming_rx.recv().await)
3473        }
3474
3475        fn is_connected(&self) -> bool {
3476            self.connected.load(Ordering::Acquire)
3477        }
3478
3479        async fn close(&mut self) -> Result<()> {
3480            self.connected.store(false, Ordering::Release);
3481            Ok(())
3482        }
3483    }
3484
3485    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3486    #[tokio::test]
3487    async fn final_subscriptions_correlate_and_cancel_over_message_transport() {
3488        let transport = SubscriptionTestTransport::new();
3489        let outgoing = transport.outgoing.clone();
3490        let incoming = transport.incoming_tx.clone();
3491        let received = Arc::new(Mutex::new(Vec::new()));
3492
3493        struct RecordingHandler(Arc<Mutex<Vec<ServerNotification>>>);
3494
3495        #[async_trait]
3496        impl ClientHandler for RecordingHandler {
3497            async fn on_notification(&self, notification: ServerNotification) {
3498                self.0.lock().unwrap().push(notification);
3499            }
3500        }
3501
3502        let client = McpClient::builder()
3503            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3504            .connect(transport, RecordingHandler(received.clone()))
3505            .await
3506            .unwrap();
3507        client.discover("test-client", "1.0.0").await.unwrap();
3508
3509        let requested = SubscriptionFilter {
3510            tools_list_changed: Some(true),
3511            ..Default::default()
3512        };
3513        let mut first = client
3514            .listen_subscriptions(requested.clone())
3515            .await
3516            .unwrap();
3517        let mut second = client.listen_subscriptions(requested).await.unwrap();
3518        assert_ne!(first.id(), second.id());
3519        assert_eq!(
3520            first.acknowledged().await.unwrap().tools_list_changed,
3521            Some(true)
3522        );
3523        assert_eq!(
3524            second.acknowledged().await.unwrap().tools_list_changed,
3525            Some(true)
3526        );
3527
3528        for _ in 0..100 {
3529            if received
3530                .lock()
3531                .unwrap()
3532                .iter()
3533                .filter(|notification| {
3534                    matches!(
3535                        notification,
3536                        ServerNotification::Subscription {
3537                            notification,
3538                            ..
3539                        } if matches!(notification.as_ref(), ServerNotification::ToolsListChanged)
3540                    )
3541                })
3542                .count()
3543                == 2
3544            {
3545                break;
3546            }
3547            tokio::task::yield_now().await;
3548        }
3549        let subscription_ids: Vec<RequestId> = received
3550            .lock()
3551            .unwrap()
3552            .iter()
3553            .filter_map(|notification| match notification {
3554                ServerNotification::Subscription {
3555                    subscription_id,
3556                    notification,
3557                } if matches!(notification.as_ref(), ServerNotification::ToolsListChanged) => {
3558                    Some(subscription_id.clone())
3559                }
3560                _ => None,
3561            })
3562            .collect();
3563        assert_eq!(subscription_ids, [first.id().clone(), second.id().clone()]);
3564
3565        incoming
3566            .send(
3567                serde_json::json!({
3568                    "jsonrpc": "2.0",
3569                    "method": notifications::CANCELLED,
3570                    "params": {
3571                        "requestId": 999,
3572                        "reason": "not a subscription"
3573                    }
3574                })
3575                .to_string(),
3576            )
3577            .await
3578            .unwrap();
3579        tokio::task::yield_now().await;
3580        assert!(
3581            !received.lock().unwrap().iter().any(|notification| matches!(
3582                notification,
3583                ServerNotification::SubscriptionCancelled { subscription_id, .. }
3584                    if subscription_id == &RequestId::Number(999)
3585            ))
3586        );
3587
3588        let first_id = first.id().clone();
3589        let second_id = second.id().clone();
3590        first.cancel().await.unwrap();
3591        second.cancel().await.unwrap();
3592        let cancellation_ids: Vec<RequestId> = outgoing
3593            .lock()
3594            .unwrap()
3595            .iter()
3596            .filter_map(|message| {
3597                let value: serde_json::Value = serde_json::from_str(message).unwrap();
3598                (value["method"] == notifications::CANCELLED)
3599                    .then(|| serde_json::from_value(value["params"]["requestId"].clone()).unwrap())
3600            })
3601            .collect();
3602        assert_eq!(cancellation_ids, [first_id, second_id]);
3603    }
3604
3605    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3606    #[tokio::test]
3607    async fn final_subscription_observes_graceful_completion() {
3608        let client = McpClient::builder()
3609            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3610            .connect_simple(SubscriptionTestTransport::new())
3611            .await
3612            .unwrap();
3613        client.discover("test-client", "1.0.0").await.unwrap();
3614
3615        let mut handle = client
3616            .listen_subscriptions(SubscriptionFilter {
3617                prompts_list_changed: Some(true),
3618                ..Default::default()
3619            })
3620            .await
3621            .unwrap();
3622        assert_eq!(
3623            handle.acknowledged().await.unwrap().prompts_list_changed,
3624            Some(true)
3625        );
3626        let expected_id = handle.id().clone();
3627        let result = handle.wait().await.unwrap();
3628        assert!(result.result_type.is_complete());
3629        assert_eq!(result.meta.subscription_id, expected_id);
3630    }
3631
3632    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3633    #[tokio::test]
3634    async fn final_subscription_accepts_server_cancellation_only_for_active_listen() {
3635        let transport = SubscriptionTestTransport::new();
3636        let incoming = transport.incoming_tx.clone();
3637        let client = McpClient::builder()
3638            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3639            .connect_simple(transport)
3640            .await
3641            .unwrap();
3642        client.discover("test-client", "1.0.0").await.unwrap();
3643
3644        let mut handle = client
3645            .listen_subscriptions(SubscriptionFilter {
3646                tools_list_changed: Some(true),
3647                ..Default::default()
3648            })
3649            .await
3650            .unwrap();
3651        handle.acknowledged().await.unwrap();
3652        let id = handle.id().clone();
3653        incoming
3654            .send(
3655                serde_json::json!({
3656                    "jsonrpc": "2.0",
3657                    "method": notifications::CANCELLED,
3658                    "params": {
3659                        "requestId": id,
3660                        "reason": "server shutdown"
3661                    }
3662                })
3663                .to_string(),
3664            )
3665            .await
3666            .unwrap();
3667
3668        let error = handle.wait().await.unwrap_err();
3669        assert!(
3670            error
3671                .to_string()
3672                .contains("subscription cancelled by server: server shutdown")
3673        );
3674    }
3675
3676    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3677    fn cacheable_tools_result(name: &str) -> serde_json::Value {
3678        serde_json::json!({
3679            "resultType": "complete",
3680            "tools": [{
3681                "name": name,
3682                "inputSchema": {
3683                    "type": "object",
3684                    "properties": {}
3685                }
3686            }],
3687            "ttlMs": 60_000,
3688            "cacheScope": "private"
3689        })
3690    }
3691
3692    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3693    #[tokio::test]
3694    async fn final_cache_serves_a_fresh_list_without_a_round_trip() {
3695        let transport = MockTransport::with_responses(vec![
3696            final_discover_result(),
3697            cacheable_tools_result("cached"),
3698        ]);
3699        let outgoing = transport.outgoing.clone();
3700        let client = McpClient::builder()
3701            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3702            .connect_simple(transport)
3703            .await
3704            .unwrap();
3705        client.discover("test-client", "1.0.0").await.unwrap();
3706
3707        let first = client.list_tools().await.unwrap();
3708        let second = client.list_tools().await.unwrap();
3709
3710        assert_eq!(first.tools[0].name, "cached");
3711        assert_eq!(second.tools[0].name, "cached");
3712        assert_eq!(outgoing.lock().unwrap().len(), 2);
3713        assert_eq!(client.response_cache_len().await, 1);
3714    }
3715
3716    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3717    #[tokio::test]
3718    async fn disabling_final_cache_forces_each_list_request() {
3719        let transport = MockTransport::with_responses(vec![
3720            final_discover_result(),
3721            cacheable_tools_result("first"),
3722            cacheable_tools_result("second"),
3723        ]);
3724        let outgoing = transport.outgoing.clone();
3725        let client = McpClient::builder()
3726            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3727            .disable_response_cache()
3728            .connect_simple(transport)
3729            .await
3730            .unwrap();
3731        client.discover("test-client", "1.0.0").await.unwrap();
3732
3733        assert_eq!(client.list_tools().await.unwrap().tools[0].name, "first");
3734        assert_eq!(client.list_tools().await.unwrap().tools[0].name, "second");
3735        assert_eq!(outgoing.lock().unwrap().len(), 3);
3736        assert_eq!(client.response_cache_len().await, 0);
3737    }
3738
3739    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3740    #[tokio::test]
3741    async fn list_changed_notification_invalidates_a_fresh_entry() {
3742        let transport = MockTransport::with_responses(vec![
3743            final_discover_result(),
3744            cacheable_tools_result("first"),
3745            cacheable_tools_result("second"),
3746        ]);
3747        let incoming = transport.incoming_tx.clone();
3748        let outgoing = transport.outgoing.clone();
3749        let notification_seen = Arc::new(AtomicBool::new(false));
3750        let handler = NotificationHandler::new().on_tools_changed({
3751            let notification_seen = notification_seen.clone();
3752            move || notification_seen.store(true, Ordering::Release)
3753        });
3754        let client = McpClient::builder()
3755            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3756            .connect(transport, handler)
3757            .await
3758            .unwrap();
3759        client.discover("test-client", "1.0.0").await.unwrap();
3760        assert_eq!(client.list_tools().await.unwrap().tools[0].name, "first");
3761
3762        incoming
3763            .send(
3764                serde_json::json!({
3765                    "jsonrpc": "2.0",
3766                    "method": notifications::TOOLS_LIST_CHANGED,
3767                    "params": {}
3768                })
3769                .to_string(),
3770            )
3771            .await
3772            .unwrap();
3773        for _ in 0..100 {
3774            if notification_seen.load(Ordering::Acquire) {
3775                break;
3776            }
3777            tokio::task::yield_now().await;
3778        }
3779        assert!(notification_seen.load(Ordering::Acquire));
3780
3781        assert_eq!(client.list_tools().await.unwrap().tools[0].name, "second");
3782        assert_eq!(outgoing.lock().unwrap().len(), 3);
3783    }
3784
3785    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3786    #[tokio::test]
3787    async fn rotating_private_partition_refetches_resource() {
3788        let resource_result = |text: &str| {
3789            serde_json::json!({
3790                "resultType": "complete",
3791                "contents": [{
3792                    "uri": "config://app",
3793                    "text": text
3794                }],
3795                "ttlMs": 60_000,
3796                "cacheScope": "private"
3797            })
3798        };
3799        let transport = MockTransport::with_responses(vec![
3800            final_discover_result(),
3801            resource_result("principal-a"),
3802            resource_result("principal-b"),
3803        ]);
3804        let outgoing = transport.outgoing.clone();
3805        let client = McpClient::builder()
3806            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3807            .response_cache(ClientCacheConfig::default().with_partition("principal-a"))
3808            .connect_simple(transport)
3809            .await
3810            .unwrap();
3811        client.discover("test-client", "1.0.0").await.unwrap();
3812
3813        assert_eq!(
3814            client
3815                .read_resource("config://app")
3816                .await
3817                .unwrap()
3818                .first_text(),
3819            Some("principal-a")
3820        );
3821        client.set_cache_partition("principal-b").await;
3822        assert_eq!(
3823            client
3824                .read_resource("config://app")
3825                .await
3826                .unwrap()
3827                .first_text(),
3828            Some("principal-b")
3829        );
3830        assert_eq!(outgoing.lock().unwrap().len(), 3);
3831    }
3832
3833    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3834    #[tokio::test]
3835    async fn final_tool_call_refreshes_stale_schema_and_retries_once() {
3836        let transport = MockTransport::with_replies(vec![
3837            MockReply::Result(final_discover_result()),
3838            MockReply::Result(cacheable_tools_result("changing-tool")),
3839            MockReply::Error(JsonRpcError::header_mismatch("stale x-mcp-header mapping")),
3840            MockReply::Result(cacheable_tools_result("changing-tool")),
3841            MockReply::Result(serde_json::json!({
3842                "resultType": "complete",
3843                "content": [{"type": "text", "text": "retried"}]
3844            })),
3845        ]);
3846        let outgoing = transport.outgoing.clone();
3847        let client = McpClient::builder()
3848            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3849            .connect_simple(transport)
3850            .await
3851            .unwrap();
3852        client.discover("test-client", "1.0.0").await.unwrap();
3853        client.list_tools().await.unwrap();
3854
3855        let result = client
3856            .call_tool("changing-tool", serde_json::json!({}))
3857            .await
3858            .unwrap();
3859        assert_eq!(result.first_text(), Some("retried"));
3860
3861        let methods: Vec<String> = outgoing
3862            .lock()
3863            .unwrap()
3864            .iter()
3865            .map(|message| {
3866                serde_json::from_str::<serde_json::Value>(message).unwrap()["method"]
3867                    .as_str()
3868                    .unwrap()
3869                    .to_string()
3870            })
3871            .collect();
3872        assert_eq!(
3873            methods,
3874            [
3875                "server/discover",
3876                "tools/list",
3877                "tools/call",
3878                "tools/list",
3879                "tools/call"
3880            ]
3881        );
3882    }
3883
3884    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3885    #[tokio::test]
3886    async fn final_call_tool_as_task_sends_no_legacy_task_parameter() {
3887        let transport = MockTransport::with_responses(vec![
3888            final_discover_result(),
3889            serde_json::json!({
3890                "resultType": "task",
3891                "taskId": "task-final",
3892                "status": "working",
3893                "createdAt": "2026-07-31T00:00:00Z",
3894                "lastUpdatedAt": "2026-07-31T00:00:00Z",
3895                "ttlMs": null,
3896                "pollIntervalMs": 50
3897            }),
3898        ]);
3899        let outgoing = transport.outgoing.clone();
3900        let client = McpClient::builder()
3901            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3902            .with_tasks()
3903            .connect_simple(transport)
3904            .await
3905            .unwrap();
3906        client.discover("test-client", "1.0.0").await.unwrap();
3907
3908        let created = client
3909            .call_tool_as_task("long-tool", serde_json::json!({}), None)
3910            .await
3911            .unwrap();
3912        assert_eq!(created.task.task_id, "task-final");
3913
3914        let messages = outgoing.lock().unwrap();
3915        let call: serde_json::Value = serde_json::from_str(&messages[1]).unwrap();
3916        assert_eq!(call["method"], "tools/call");
3917        assert!(
3918            call["params"].get("task").is_none(),
3919            "final tools/call leaked the legacy task parameter: {call}"
3920        );
3921        assert!(
3922            call["params"]["_meta"]["io.modelcontextprotocol/clientCapabilities"]["extensions"]
3923                .get(crate::protocol::TASKS_EXTENSION_ID)
3924                .is_some()
3925        );
3926    }
3927
3928    #[cfg(any(feature = "protocol-2026-07-28", feature = "stateless"))]
3929    #[tokio::test]
3930    async fn final_stale_schema_retry_is_bounded() {
3931        let transport = MockTransport::with_replies(vec![
3932            MockReply::Result(final_discover_result()),
3933            MockReply::Result(cacheable_tools_result("changing-tool")),
3934            MockReply::Error(JsonRpcError::header_mismatch("first rejection")),
3935            MockReply::Result(cacheable_tools_result("changing-tool")),
3936            MockReply::Error(JsonRpcError::header_mismatch("second rejection")),
3937        ]);
3938        let outgoing = transport.outgoing.clone();
3939        let client = McpClient::builder()
3940            .protocol_support(ProtocolSupport::try_new(["2026-07-28"]).unwrap())
3941            .connect_simple(transport)
3942            .await
3943            .unwrap();
3944        client.discover("test-client", "1.0.0").await.unwrap();
3945        client.list_tools().await.unwrap();
3946
3947        let error = client
3948            .call_tool("changing-tool", serde_json::json!({}))
3949            .await
3950            .unwrap_err();
3951        assert!(matches!(
3952            error,
3953            Error::JsonRpc(error) if error.code == McpErrorCode::HeaderMismatch.code()
3954        ));
3955        assert_eq!(outgoing.lock().unwrap().len(), 5);
3956    }
3957
3958    #[tokio::test]
3959    async fn legacy_tool_call_does_not_retry_a_header_mismatch() {
3960        let transport = MockTransport::with_replies(vec![
3961            MockReply::Result(mock_initialize_response()),
3962            MockReply::Error(JsonRpcError::header_mismatch("legacy rejection")),
3963        ]);
3964        let outgoing = transport.outgoing.clone();
3965        let client = McpClient::connect(transport).await.unwrap();
3966        client.initialize("test-client", "1.0.0").await.unwrap();
3967
3968        let error = client
3969            .call_tool("changing-tool", serde_json::json!({}))
3970            .await
3971            .unwrap_err();
3972        assert!(matches!(
3973            error,
3974            Error::JsonRpc(error) if error.code == McpErrorCode::HeaderMismatch.code()
3975        ));
3976        let messages = outgoing.lock().unwrap();
3977        assert_eq!(messages.len(), 3);
3978        assert!(
3979            !messages
3980                .iter()
3981                .any(|message| message.contains("tools/list"))
3982        );
3983    }
3984
3985    #[test]
3986    fn stale_tool_schema_errors_are_pre_execution_protocol_errors() {
3987        for error in [
3988            Error::JsonRpc(JsonRpcError::header_mismatch("mismatch")),
3989            Error::JsonRpc(JsonRpcError::method_not_found("tool")),
3990            Error::JsonRpc(JsonRpcError::invalid_params("arguments")),
3991        ] {
3992            assert!(is_stale_tool_schema_error(&error));
3993        }
3994        assert!(!is_stale_tool_schema_error(&Error::JsonRpc(
3995            JsonRpcError::internal_error("executed")
3996        )));
3997    }
3998
3999    #[tokio::test]
4000    async fn test_list_tools() {
4001        let client = McpClient::connect(MockTransport::with_responses(vec![
4002            mock_initialize_response(),
4003            serde_json::json!({
4004                "tools": [
4005                    {
4006                        "name": "test_tool",
4007                        "description": "A test tool",
4008                        "inputSchema": {
4009                            "type": "object",
4010                            "properties": {}
4011                        }
4012                    }
4013                ]
4014            }),
4015        ]))
4016        .await
4017        .unwrap();
4018
4019        client.initialize("test-client", "1.0.0").await.unwrap();
4020        let tools = client.list_tools().await.unwrap();
4021
4022        assert_eq!(tools.tools.len(), 1);
4023        assert_eq!(tools.tools[0].name, "test_tool");
4024    }
4025
4026    #[tokio::test]
4027    async fn test_call_tool() {
4028        let client = McpClient::connect(MockTransport::with_responses(vec![
4029            mock_initialize_response(),
4030            serde_json::json!({
4031                "content": [
4032                    {
4033                        "type": "text",
4034                        "text": "Tool result"
4035                    }
4036                ]
4037            }),
4038        ]))
4039        .await
4040        .unwrap();
4041
4042        client.initialize("test-client", "1.0.0").await.unwrap();
4043        let result = client
4044            .call_tool("test_tool", serde_json::json!({"arg": "value"}))
4045            .await
4046            .unwrap();
4047
4048        assert!(!result.content.is_empty());
4049    }
4050
4051    #[tokio::test]
4052    async fn test_list_resources() {
4053        let client = McpClient::connect(MockTransport::with_responses(vec![
4054            mock_initialize_response(),
4055            serde_json::json!({
4056                "resources": [
4057                    {
4058                        "uri": "file://test.txt",
4059                        "name": "Test File"
4060                    }
4061                ]
4062            }),
4063        ]))
4064        .await
4065        .unwrap();
4066
4067        client.initialize("test-client", "1.0.0").await.unwrap();
4068        let resources = client.list_resources().await.unwrap();
4069
4070        assert_eq!(resources.resources.len(), 1);
4071        assert_eq!(resources.resources[0].uri, "file://test.txt");
4072    }
4073
4074    #[tokio::test]
4075    async fn test_read_resource() {
4076        let client = McpClient::connect(MockTransport::with_responses(vec![
4077            mock_initialize_response(),
4078            serde_json::json!({
4079                "contents": [
4080                    {
4081                        "uri": "file://test.txt",
4082                        "text": "File contents"
4083                    }
4084                ]
4085            }),
4086        ]))
4087        .await
4088        .unwrap();
4089
4090        client.initialize("test-client", "1.0.0").await.unwrap();
4091        let result = client.read_resource("file://test.txt").await.unwrap();
4092
4093        assert_eq!(result.contents.len(), 1);
4094        assert_eq!(result.contents[0].text.as_deref(), Some("File contents"));
4095    }
4096
4097    #[tokio::test]
4098    async fn test_list_prompts() {
4099        let client = McpClient::connect(MockTransport::with_responses(vec![
4100            mock_initialize_response(),
4101            serde_json::json!({
4102                "prompts": [
4103                    {
4104                        "name": "test_prompt",
4105                        "description": "A test prompt"
4106                    }
4107                ]
4108            }),
4109        ]))
4110        .await
4111        .unwrap();
4112
4113        client.initialize("test-client", "1.0.0").await.unwrap();
4114        let prompts = client.list_prompts().await.unwrap();
4115
4116        assert_eq!(prompts.prompts.len(), 1);
4117        assert_eq!(prompts.prompts[0].name, "test_prompt");
4118    }
4119
4120    #[tokio::test]
4121    async fn test_get_prompt() {
4122        let client = McpClient::connect(MockTransport::with_responses(vec![
4123            mock_initialize_response(),
4124            serde_json::json!({
4125                "messages": [
4126                    {
4127                        "role": "user",
4128                        "content": {
4129                            "type": "text",
4130                            "text": "Prompt message"
4131                        }
4132                    }
4133                ]
4134            }),
4135        ]))
4136        .await
4137        .unwrap();
4138
4139        client.initialize("test-client", "1.0.0").await.unwrap();
4140        let result = client.get_prompt("test_prompt", None).await.unwrap();
4141
4142        assert_eq!(result.messages.len(), 1);
4143    }
4144
4145    #[tokio::test]
4146    async fn test_ping() {
4147        let client = McpClient::connect(MockTransport::with_responses(vec![
4148            mock_initialize_response(),
4149            serde_json::json!({}),
4150        ]))
4151        .await
4152        .unwrap();
4153
4154        client.initialize("test-client", "1.0.0").await.unwrap();
4155        let result = client.ping().await;
4156        assert!(result.is_ok());
4157    }
4158
4159    #[tokio::test]
4160    async fn test_with_roots() {
4161        let roots = vec![Root::new("file:///test")];
4162        let client = McpClient::builder()
4163            .with_roots(roots)
4164            .connect_simple(MockTransport::with_responses(vec![]))
4165            .await
4166            .unwrap();
4167
4168        let current_roots = client.roots().await;
4169        assert_eq!(current_roots.len(), 1);
4170    }
4171
4172    #[tokio::test]
4173    async fn test_roots_management() {
4174        let client = McpClient::connect(MockTransport::with_responses(vec![
4175            mock_initialize_response(),
4176        ]))
4177        .await
4178        .unwrap();
4179
4180        // Initially no roots
4181        assert!(client.roots().await.is_empty());
4182
4183        // Add a root before initialization (no notification sent)
4184        client.add_root(Root::new("file:///project")).await.unwrap();
4185        assert_eq!(client.roots().await.len(), 1);
4186
4187        // Initialize
4188        client.initialize("test-client", "1.0.0").await.unwrap();
4189
4190        // Remove a root
4191        let removed = client.remove_root("file:///project").await.unwrap();
4192        assert!(removed);
4193        assert!(client.roots().await.is_empty());
4194
4195        // Try to remove non-existent root
4196        let not_removed = client.remove_root("file:///nonexistent").await.unwrap();
4197        assert!(!not_removed);
4198    }
4199
4200    #[tokio::test]
4201    async fn test_list_roots() {
4202        let roots = vec![
4203            Root::new("file:///project1"),
4204            Root::with_name("file:///project2", "Project 2"),
4205        ];
4206        let client = McpClient::builder()
4207            .with_roots(roots)
4208            .connect_simple(MockTransport::with_responses(vec![]))
4209            .await
4210            .unwrap();
4211
4212        let result = client.list_roots().await;
4213        assert_eq!(result.roots.len(), 2);
4214        assert_eq!(result.roots[1].name, Some("Project 2".to_string()));
4215    }
4216
4217    #[test]
4218    fn test_builder_with_sampling() {
4219        let builder = McpClientBuilder::new().with_sampling();
4220        assert!(builder.capabilities.sampling.is_some());
4221    }
4222
4223    #[test]
4224    fn test_builder_with_elicitation() {
4225        let builder = McpClientBuilder::new().with_elicitation();
4226        assert!(builder.capabilities.elicitation.is_some());
4227    }
4228
4229    #[test]
4230    fn builder_adds_protocol_extension_without_replacing_other_capabilities() {
4231        let extension = crate::ExtensionDeclaration::new(
4232            "com.example/rendering",
4233            serde_json::json!({"formats": ["html"]}),
4234        )
4235        .unwrap();
4236        let builder = McpClientBuilder::new()
4237            .with_sampling()
4238            .with_protocol_extension(extension);
4239
4240        assert!(builder.capabilities.sampling.is_some());
4241        assert_eq!(
4242            builder.capabilities.extensions.as_ref().unwrap()["com.example/rendering"]["formats"]
4243                [0],
4244            "html"
4245        );
4246    }
4247
4248    #[test]
4249    fn test_builder_chaining() {
4250        let builder = McpClientBuilder::new()
4251            .with_sampling()
4252            .with_elicitation()
4253            .with_roots(vec![Root::new("file:///project")]);
4254        assert!(builder.capabilities.sampling.is_some());
4255        assert!(builder.capabilities.elicitation.is_some());
4256        assert!(builder.capabilities.roots.is_some());
4257    }
4258
4259    #[tokio::test]
4260    async fn test_bidirectional_sampling_round_trip() {
4261        use crate::protocol::{
4262            ContentRole, CreateMessageParams, CreateMessageResult, SamplingContent,
4263            SamplingContentOrArray,
4264        };
4265
4266        // A handler that records whether handle_create_message was called
4267        struct RecordingHandler {
4268            called: Arc<AtomicBool>,
4269        }
4270
4271        #[async_trait]
4272        impl ClientHandler for RecordingHandler {
4273            async fn handle_create_message(
4274                &self,
4275                _params: CreateMessageParams,
4276            ) -> std::result::Result<CreateMessageResult, tower_mcp_types::JsonRpcError>
4277            {
4278                self.called.store(true, Ordering::SeqCst);
4279                Ok(CreateMessageResult {
4280                    content: SamplingContentOrArray::Single(SamplingContent::Text {
4281                        text: "test response".to_string(),
4282                        annotations: None,
4283                        meta: None,
4284                    }),
4285                    model: "test-model".to_string(),
4286                    role: ContentRole::Assistant,
4287                    stop_reason: Some("end_turn".to_string()),
4288                    meta: None,
4289                })
4290            }
4291        }
4292
4293        let called = Arc::new(AtomicBool::new(false));
4294        let handler = RecordingHandler {
4295            called: called.clone(),
4296        };
4297
4298        // Build a mock transport, keeping a clone of incoming_tx so we can
4299        // inject a server-initiated request after the transport is consumed.
4300        let (inject_tx, rx) = mpsc::channel::<String>(32);
4301        let responses = vec![mock_initialize_response()];
4302        let inject_tx_clone = inject_tx.clone();
4303
4304        let transport = MockTransport {
4305            responses: Arc::new(Mutex::new(
4306                responses.into_iter().map(MockReply::Result).collect(),
4307            )),
4308            response_idx: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
4309            incoming_tx: inject_tx,
4310            incoming_rx: rx,
4311            outgoing: Arc::new(Mutex::new(Vec::new())),
4312            connected: Arc::new(AtomicBool::new(true)),
4313            fail_notification_sends: Arc::new(AtomicBool::new(false)),
4314        };
4315
4316        let client = McpClient::builder()
4317            .with_sampling()
4318            .connect(transport, handler)
4319            .await
4320            .unwrap();
4321
4322        // Initialize the client (this sends initialize request + notification)
4323        client.initialize("test-client", "1.0.0").await.unwrap();
4324
4325        // Inject a server-initiated sampling/createMessage request
4326        let sampling_request = serde_json::json!({
4327            "jsonrpc": "2.0",
4328            "id": 100,
4329            "method": "sampling/createMessage",
4330            "params": {
4331                "messages": [
4332                    {
4333                        "role": "user",
4334                        "content": {
4335                            "type": "text",
4336                            "text": "Hello"
4337                        }
4338                    }
4339                ],
4340                "maxTokens": 100
4341            }
4342        });
4343        inject_tx_clone
4344            .send(sampling_request.to_string())
4345            .await
4346            .unwrap();
4347
4348        // Give the background loop time to process
4349        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
4350
4351        // Verify the handler was called
4352        assert!(
4353            called.load(Ordering::SeqCst),
4354            "handle_create_message should have been called"
4355        );
4356    }
4357
4358    #[tokio::test]
4359    async fn test_list_resource_templates() {
4360        let client = McpClient::connect(MockTransport::with_responses(vec![
4361            mock_initialize_response(),
4362            serde_json::json!({
4363                "resourceTemplates": [
4364                    {
4365                        "uriTemplate": "file:///{path}",
4366                        "name": "File Template",
4367                        "description": "A file template"
4368                    }
4369                ]
4370            }),
4371        ]))
4372        .await
4373        .unwrap();
4374
4375        client.initialize("test-client", "1.0.0").await.unwrap();
4376        let result = client.list_resource_templates().await.unwrap();
4377
4378        assert_eq!(result.resource_templates.len(), 1);
4379        assert_eq!(result.resource_templates[0].name, "File Template");
4380    }
4381
4382    #[tokio::test]
4383    async fn test_list_all_tools_single_page() {
4384        let client = McpClient::connect(MockTransport::with_responses(vec![
4385            mock_initialize_response(),
4386            serde_json::json!({
4387                "tools": [
4388                    {
4389                        "name": "tool_a",
4390                        "description": "Tool A",
4391                        "inputSchema": { "type": "object", "properties": {} }
4392                    },
4393                    {
4394                        "name": "tool_b",
4395                        "description": "Tool B",
4396                        "inputSchema": { "type": "object", "properties": {} }
4397                    }
4398                ]
4399            }),
4400        ]))
4401        .await
4402        .unwrap();
4403
4404        client.initialize("test-client", "1.0.0").await.unwrap();
4405        let tools = client.list_all_tools().await.unwrap();
4406
4407        assert_eq!(tools.len(), 2);
4408        assert_eq!(tools[0].name, "tool_a");
4409        assert_eq!(tools[1].name, "tool_b");
4410    }
4411
4412    #[tokio::test]
4413    async fn test_list_all_tools_paginated() {
4414        let client = McpClient::connect(MockTransport::with_responses(vec![
4415            mock_initialize_response(),
4416            // First page with a next_cursor
4417            serde_json::json!({
4418                "tools": [
4419                    {
4420                        "name": "tool_a",
4421                        "description": "Tool A",
4422                        "inputSchema": { "type": "object", "properties": {} }
4423                    }
4424                ],
4425                "nextCursor": "page2"
4426            }),
4427            // Second page with no next_cursor
4428            serde_json::json!({
4429                "tools": [
4430                    {
4431                        "name": "tool_b",
4432                        "description": "Tool B",
4433                        "inputSchema": { "type": "object", "properties": {} }
4434                    }
4435                ]
4436            }),
4437        ]))
4438        .await
4439        .unwrap();
4440
4441        client.initialize("test-client", "1.0.0").await.unwrap();
4442        let tools = client.list_all_tools().await.unwrap();
4443
4444        assert_eq!(tools.len(), 2);
4445        assert_eq!(tools[0].name, "tool_a");
4446        assert_eq!(tools[1].name, "tool_b");
4447    }
4448
4449    #[tokio::test]
4450    async fn test_call_tool_text_success() {
4451        let client = McpClient::connect(MockTransport::with_responses(vec![
4452            mock_initialize_response(),
4453            serde_json::json!({
4454                "content": [
4455                    { "type": "text", "text": "Hello " },
4456                    { "type": "text", "text": "World" }
4457                ]
4458            }),
4459        ]))
4460        .await
4461        .unwrap();
4462
4463        client.initialize("test-client", "1.0.0").await.unwrap();
4464        let text = client
4465            .call_tool_text("test_tool", serde_json::json!({}))
4466            .await
4467            .unwrap();
4468
4469        assert_eq!(text, "Hello World");
4470    }
4471
4472    #[tokio::test]
4473    async fn test_call_tool_text_error() {
4474        let client = McpClient::connect(MockTransport::with_responses(vec![
4475            mock_initialize_response(),
4476            serde_json::json!({
4477                "content": [
4478                    { "type": "text", "text": "something went wrong" }
4479                ],
4480                "isError": true
4481            }),
4482        ]))
4483        .await
4484        .unwrap();
4485
4486        client.initialize("test-client", "1.0.0").await.unwrap();
4487        let result = client
4488            .call_tool_text("test_tool", serde_json::json!({}))
4489            .await;
4490
4491        assert!(result.is_err());
4492        let err = result.unwrap_err();
4493        assert!(
4494            err.to_string().contains("something went wrong"),
4495            "Error message should contain tool error text, got: {}",
4496            err
4497        );
4498    }
4499
4500    #[tokio::test]
4501    async fn test_server_notification_parsing() {
4502        let notification = parse_server_notification("notifications/tools/list_changed", None);
4503        assert!(matches!(notification, ServerNotification::ToolsListChanged));
4504
4505        let notification = parse_server_notification("notifications/resources/list_changed", None);
4506        assert!(matches!(
4507            notification,
4508            ServerNotification::ResourcesListChanged
4509        ));
4510
4511        let notification = parse_server_notification(
4512            "notifications/resources/updated",
4513            Some(serde_json::json!({"uri": "file:///test"})),
4514        );
4515        match notification {
4516            ServerNotification::ResourceUpdated { uri } => {
4517                assert_eq!(uri, "file:///test");
4518            }
4519            _ => panic!("Expected ResourceUpdated"),
4520        }
4521
4522        let notification =
4523            parse_server_notification("custom/notification", Some(serde_json::json!({"data": 42})));
4524        match notification {
4525            ServerNotification::Unknown { method, params } => {
4526                assert_eq!(method, "custom/notification");
4527                assert!(params.is_some());
4528            }
4529            _ => panic!("Expected Unknown"),
4530        }
4531
4532        let notification = parse_server_notification(
4533            notifications::SUBSCRIPTIONS_ACKNOWLEDGED,
4534            Some(serde_json::json!({
4535                "_meta": {
4536                    "io.modelcontextprotocol/subscriptionId": 7
4537                },
4538                "notifications": {
4539                    "toolsListChanged": true
4540                }
4541            })),
4542        );
4543        assert!(matches!(
4544            notification,
4545            ServerNotification::SubscriptionAcknowledged {
4546                subscription_id: RequestId::Number(7),
4547                ..
4548            }
4549        ));
4550
4551        let notification = parse_server_notification(
4552            notifications::TOOLS_LIST_CHANGED,
4553            Some(serde_json::json!({
4554                "_meta": {
4555                    "io.modelcontextprotocol/subscriptionId": "stream-a"
4556                }
4557            })),
4558        );
4559        assert!(matches!(
4560            notification,
4561            ServerNotification::Subscription {
4562                subscription_id: RequestId::String(id),
4563                notification,
4564            } if id == "stream-a"
4565                && matches!(notification.as_ref(), ServerNotification::ToolsListChanged)
4566        ));
4567
4568        let notification = parse_server_notification(
4569            notifications::CANCELLED,
4570            Some(serde_json::json!({
4571                "requestId": 7,
4572                "reason": "done"
4573            })),
4574        );
4575        assert!(matches!(
4576            notification,
4577            ServerNotification::SubscriptionCancelled {
4578                subscription_id: RequestId::Number(7),
4579                reason: Some(reason),
4580            } if reason == "done"
4581        ));
4582
4583        let notification = parse_server_notification(
4584            notifications::TASK_STATUS_CHANGED,
4585            Some(serde_json::json!({
4586                "taskId": "legacy-task",
4587                "status": "completed",
4588                "createdAt": "2026-08-02T00:00:00Z",
4589                "lastUpdatedAt": "2026-08-02T00:00:01Z",
4590                "ttl": null
4591            })),
4592        );
4593        assert!(matches!(
4594            notification,
4595            ServerNotification::TaskStatusChanged(TaskStatusParams {
4596                task_id,
4597                status: crate::protocol::TaskStatus::Completed,
4598                ..
4599            }) if task_id == "legacy-task"
4600        ));
4601
4602        let notification = parse_server_notification(
4603            notifications::TASK_STATUS_CHANGED,
4604            Some(serde_json::json!({
4605                "taskId": "final-task",
4606                "status": "cancelled",
4607                "createdAt": "2026-08-02T00:00:00Z",
4608                "lastUpdatedAt": "2026-08-02T00:00:01Z",
4609                "ttlMs": null,
4610                "_meta": {
4611                    "io.modelcontextprotocol/subscriptionId": "task-stream"
4612                }
4613            })),
4614        );
4615        assert!(matches!(
4616            notification,
4617            ServerNotification::Subscription {
4618                subscription_id: RequestId::String(id),
4619                notification,
4620            } if id == "task-stream"
4621                && matches!(
4622                    notification.as_ref(),
4623                    ServerNotification::FinalTaskStatusChanged(params)
4624                        if params.task.task_id() == "final-task"
4625                            && params.task.status() == crate::protocol::TaskStatus::Cancelled
4626                )
4627        ));
4628    }
4629
4630    // =========================================================================
4631    // handle_response ID correlation
4632    // =========================================================================
4633
4634    fn pending_with(
4635        ids: &[RequestId],
4636    ) -> (
4637        HashMap<RequestId, PendingRequest>,
4638        Vec<oneshot::Receiver<Result<serde_json::Value>>>,
4639    ) {
4640        let mut map = HashMap::new();
4641        let mut rxs = Vec::new();
4642        for id in ids {
4643            let (tx, rx) = oneshot::channel();
4644            map.insert(
4645                id.clone(),
4646                PendingRequest {
4647                    method: "test".to_string(),
4648                    response_tx: tx,
4649                    acknowledgment_tx: None,
4650                },
4651            );
4652            rxs.push(rx);
4653        }
4654        (map, rxs)
4655    }
4656
4657    #[tokio::test]
4658    async fn test_stringified_numeric_response_id_correlates() {
4659        // rmcp #1021 analog: a numeric request id 42 answered with a
4660        // stringified id "42" still correlates.
4661        let (mut pending, mut rxs) = pending_with(&[RequestId::Number(42)]);
4662
4663        let response = serde_json::json!({
4664            "jsonrpc": "2.0",
4665            "id": "42",
4666            "result": {"ok": true}
4667        });
4668        handle_response(&response, &mut pending);
4669
4670        assert!(pending.is_empty(), "pending request should be resolved");
4671        let result = rxs.remove(0).await.unwrap().unwrap();
4672        assert_eq!(result, serde_json::json!({"ok": true}));
4673    }
4674
4675    #[tokio::test]
4676    async fn test_exact_string_id_takes_precedence() {
4677        // A genuine string id "42" must match exactly and win over the
4678        // numeric interpretation when both are pending.
4679        let (mut pending, mut rxs) =
4680            pending_with(&[RequestId::String("42".to_string()), RequestId::Number(42)]);
4681
4682        let response = serde_json::json!({
4683            "jsonrpc": "2.0",
4684            "id": "42",
4685            "result": {"which": "string"}
4686        });
4687        handle_response(&response, &mut pending);
4688
4689        // The string entry resolved; the numeric entry is still pending.
4690        assert_eq!(pending.len(), 1);
4691        assert!(pending.contains_key(&RequestId::Number(42)));
4692        let result = rxs.remove(0).await.unwrap().unwrap();
4693        assert_eq!(result, serde_json::json!({"which": "string"}));
4694    }
4695
4696    #[tokio::test]
4697    async fn test_non_numeric_string_id_does_not_correlate() {
4698        // A string id that is not the string form of the pending numeric
4699        // id must not resolve it.
4700        let (mut pending, _rxs) = pending_with(&[RequestId::Number(42)]);
4701
4702        let response = serde_json::json!({
4703            "jsonrpc": "2.0",
4704            "id": "not-a-number",
4705            "result": {}
4706        });
4707        handle_response(&response, &mut pending);
4708
4709        assert_eq!(pending.len(), 1, "numeric request should stay pending");
4710    }
4711}