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