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