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