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