Skip to main content

tower_mcp/client/
mod.rs

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