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