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