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