1use std::collections::HashMap;
51use std::sync::Arc;
52use std::sync::atomic::{AtomicBool, Ordering};
53use std::time::Duration;
54
55use async_trait::async_trait;
56use base64::Engine;
57use tokio::sync::{Notify, RwLock, mpsc};
58use tokio::task::JoinHandle;
59
60use super::transport::ClientTransport;
61use crate::error::{Error, Result};
62use crate::protocol::{RequestId, notifications};
63
64const MCP_METHOD_HEADER: &str = "mcp-method";
65const MCP_NAME_HEADER: &str = "mcp-name";
66const MCP_PARAM_HEADER_PREFIX: &str = "mcp-param-";
67const BASE64_SENTINEL_PREFIX: &str = "=?base64?";
68const BASE64_SENTINEL_SUFFIX: &str = "?=";
69const SESSION_DELETE_TIMEOUT: Duration = Duration::from_secs(5);
72
73#[derive(Debug, Clone)]
74struct CustomHeaderMapping {
75 suffix: String,
76 property_path: Vec<String>,
77}
78
79#[cfg(feature = "oauth-client")]
80#[derive(Clone)]
81struct ScopeEscalationRuntime {
82 handler: Arc<dyn OAuthScopeEscalationHandler>,
83 state: Arc<tokio::sync::Mutex<ScopeEscalationState>>,
84 max_attempts: usize,
85}
86
87#[cfg(feature = "oauth-client")]
88struct ScopeEscalationState {
89 scopes: Vec<String>,
90 revision: usize,
91}
92
93#[cfg(feature = "oauth-client")]
94impl ScopeEscalationRuntime {
95 fn new<P>(handler: Arc<P>, config: OAuthScopeEscalationConfig) -> Self
96 where
97 P: OAuthScopeEscalationHandler,
98 {
99 Self {
100 handler,
101 state: Arc::new(tokio::sync::Mutex::new(ScopeEscalationState {
102 scopes: config.initial_scopes().to_vec(),
103 revision: 0,
104 })),
105 max_attempts: config.maximum_attempts(),
106 }
107 }
108
109 async fn respond_to_challenge(
110 &self,
111 challenge: OAuthScopeChallenge,
112 resource: &str,
113 operation: &str,
114 attempt: usize,
115 observed_revision: usize,
116 ) -> std::result::Result<ScopeEscalationDecision, OAuthClientError> {
117 let mut state = self.state.lock().await;
121 let previous_scopes = state.scopes.clone();
122 let mut requested_scopes = previous_scopes.clone();
123 for scope in &challenge.required_scopes {
124 if !requested_scopes.contains(scope) {
125 requested_scopes.push(scope.clone());
126 }
127 }
128
129 if requested_scopes == previous_scopes && state.revision > observed_revision {
130 return Ok(ScopeEscalationDecision {
131 revision: state.revision,
132 });
133 }
134 self.handler
139 .reauthorize(OAuthScopeEscalationRequest {
140 resource: resource.to_string(),
141 operation: operation.to_string(),
142 challenge,
143 previous_scopes,
144 requested_scopes: requested_scopes.clone(),
145 attempt,
146 })
147 .await?;
148
149 state.scopes = requested_scopes;
150 state.revision += 1;
151 Ok(ScopeEscalationDecision {
152 revision: state.revision,
153 })
154 }
155}
156
157#[cfg(feature = "oauth-client")]
158struct ScopeEscalationDecision {
159 revision: usize,
160}
161
162#[cfg(feature = "oauth-client")]
163use super::oauth::{
164 OAuthBearerChallenge, OAuthClientError, OAuthScopeChallenge, OAuthScopeEscalationConfig,
165 OAuthScopeEscalationHandler, OAuthScopeEscalationRequest, TokenProvider,
166};
167
168#[derive(Debug, Clone)]
183pub struct HttpClientConfig {
184 pub headers: HashMap<String, String>,
186 pub auto_sse: bool,
191 pub channel_capacity: usize,
194 pub request_timeout: Duration,
197 pub notification_timeout: Duration,
207 pub sse_reconnect: bool,
210 pub sse_reconnect_delay: Duration,
213 pub max_sse_reconnect_attempts: u32,
216 pub session_recovery: bool,
222 pub max_sse_event_size: usize,
230 pub max_response_body_size: usize,
242 pub follow_cross_origin_redirects: bool,
267}
268
269pub const DEFAULT_MAX_SSE_EVENT_SIZE: usize = 16 * 1024 * 1024;
272
273pub const DEFAULT_MAX_RESPONSE_BODY_SIZE: usize = DEFAULT_MAX_SSE_EVENT_SIZE;
277
278impl Default for HttpClientConfig {
279 fn default() -> Self {
280 Self {
281 headers: HashMap::new(),
282 auto_sse: true,
283 channel_capacity: 256,
284 request_timeout: Duration::from_secs(30),
285 notification_timeout: Duration::from_secs(5),
286 sse_reconnect: true,
287 sse_reconnect_delay: Duration::from_secs(1),
288 max_sse_reconnect_attempts: 5,
289 session_recovery: true,
290 max_sse_event_size: DEFAULT_MAX_SSE_EVENT_SIZE,
291 max_response_body_size: DEFAULT_MAX_RESPONSE_BODY_SIZE,
292 follow_cross_origin_redirects: false,
293 }
294 }
295}
296
297impl HttpClientConfig {
298 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
300 self.headers.insert(
301 "Authorization".to_string(),
302 format!("Bearer {}", token.into()),
303 );
304 self
305 }
306
307 pub fn api_key_header(mut self, name: impl Into<String>, key: impl Into<String>) -> Self {
309 self.headers.insert(name.into(), key.into());
310 self
311 }
312
313 pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
315 use base64::Engine;
316 let encoded = base64::engine::general_purpose::STANDARD.encode(format!(
317 "{}:{}",
318 username.as_ref(),
319 password.as_ref()
320 ));
321 self.headers
322 .insert("Authorization".to_string(), format!("Basic {}", encoded));
323 self
324 }
325
326 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
328 self.headers.insert(name.into(), value.into());
329 self
330 }
331}
332
333const MAX_REDIRECT_HOPS: usize = 10;
338
339const MAX_PENDING_TOOLS_LISTS: usize = 64;
343
344fn redirect_preserves_request(status: reqwest::StatusCode) -> bool {
353 matches!(
354 status,
355 reqwest::StatusCode::TEMPORARY_REDIRECT | reqwest::StatusCode::PERMANENT_REDIRECT
356 )
357}
358
359fn should_follow_redirect(
371 original: &reqwest::Url,
372 target: &reqwest::Url,
373 allow_cross_origin: bool,
374) -> bool {
375 if original.scheme() == "https" && target.scheme() != "https" {
376 return false;
377 }
378 if allow_cross_origin {
379 return true;
380 }
381 original.scheme() == target.scheme()
382 && original.host_str() == target.host_str()
383 && original.port_or_known_default() == target.port_or_known_default()
384}
385
386fn build_client(allow_cross_origin_redirects: bool) -> reqwest::Client {
391 let policy = reqwest::redirect::Policy::custom(move |attempt| {
392 if !redirect_preserves_request(attempt.status()) {
396 return attempt.stop();
397 }
398 if attempt.previous().len() > MAX_REDIRECT_HOPS {
399 return attempt.error("too many redirects");
400 }
401 let Some(original) = attempt.previous().first() else {
404 return attempt.follow();
405 };
406 if should_follow_redirect(original, attempt.url(), allow_cross_origin_redirects) {
407 attempt.follow()
408 } else {
409 attempt.error(
410 "redirect to a different origin was not followed; set \
411 HttpClientConfig::follow_cross_origin_redirects to allow it (an https-to-http \
412 downgrade is never followed)",
413 )
414 }
415 });
416 reqwest::Client::builder()
417 .redirect(policy)
418 .build()
419 .expect("reqwest client with a custom redirect policy should always build")
420}
421
422pub struct HttpClientTransport {
464 url: String,
466 client: reqwest::Client,
468 session_id: Option<String>,
470 protocol_version: Option<String>,
472 tool_header_mappings: HashMap<String, Vec<CustomHeaderMapping>>,
475 pending_tools_lists: Vec<(serde_json::Value, bool)>,
480 incoming_rx: mpsc::Receiver<String>,
482 incoming_tx: mpsc::Sender<String>,
485 sse_task: Option<JoinHandle<()>>,
487 request_tasks: HashMap<RequestId, JoinHandle<()>>,
493 initialize_request_id: Option<RequestId>,
495 last_event_id: Arc<RwLock<Option<String>>>,
497 sse_retry_delay: Arc<RwLock<Option<Duration>>>,
499 sse_reconnect_signal: Arc<Notify>,
501 connected: Arc<AtomicBool>,
503 config: HttpClientConfig,
505 #[cfg(feature = "oauth-client")]
507 token_provider: Option<Arc<dyn TokenProvider>>,
508 #[cfg(feature = "oauth-client")]
510 scope_escalation: Option<ScopeEscalationRuntime>,
511}
512
513impl HttpClientTransport {
514 pub fn new(url: impl Into<String>) -> Self {
527 Self::with_config(url, HttpClientConfig::default())
528 }
529
530 pub fn with_config(url: impl Into<String>, config: HttpClientConfig) -> Self {
546 let (tx, rx) = mpsc::channel(config.channel_capacity);
547 Self {
548 url: url.into(),
549 client: build_client(config.follow_cross_origin_redirects),
550 session_id: None,
551 protocol_version: None,
552 tool_header_mappings: HashMap::new(),
553 pending_tools_lists: Vec::new(),
554 incoming_rx: rx,
555 incoming_tx: tx,
556 sse_task: None,
557 request_tasks: HashMap::new(),
558 initialize_request_id: None,
559 last_event_id: Arc::new(RwLock::new(None)),
560 sse_retry_delay: Arc::new(RwLock::new(None)),
561 sse_reconnect_signal: Arc::new(Notify::new()),
562 connected: Arc::new(AtomicBool::new(true)),
563 config,
564 #[cfg(feature = "oauth-client")]
565 token_provider: None,
566 #[cfg(feature = "oauth-client")]
567 scope_escalation: None,
568 }
569 }
570
571 pub fn with_client(url: impl Into<String>, client: reqwest::Client) -> Self {
588 let config = HttpClientConfig::default();
589 let (tx, rx) = mpsc::channel(config.channel_capacity);
590 Self {
591 url: url.into(),
592 client,
593 session_id: None,
594 protocol_version: None,
595 tool_header_mappings: HashMap::new(),
596 pending_tools_lists: Vec::new(),
597 incoming_rx: rx,
598 incoming_tx: tx,
599 sse_task: None,
600 request_tasks: HashMap::new(),
601 initialize_request_id: None,
602 last_event_id: Arc::new(RwLock::new(None)),
603 sse_retry_delay: Arc::new(RwLock::new(None)),
604 sse_reconnect_signal: Arc::new(Notify::new()),
605 connected: Arc::new(AtomicBool::new(true)),
606 config,
607 #[cfg(feature = "oauth-client")]
608 token_provider: None,
609 #[cfg(feature = "oauth-client")]
610 scope_escalation: None,
611 }
612 }
613
614 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
627 self.config.headers.insert(
628 "Authorization".to_string(),
629 format!("Bearer {}", token.into()),
630 );
631 self
632 }
633
634 pub fn api_key(self, key: impl Into<String>) -> Self {
648 self.bearer_token(key)
649 }
650
651 pub fn api_key_header(mut self, name: impl Into<String>, key: impl Into<String>) -> Self {
664 self.config.headers.insert(name.into(), key.into());
665 self
666 }
667
668 pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
682 use base64::Engine;
683 let encoded = base64::engine::general_purpose::STANDARD.encode(format!(
684 "{}:{}",
685 username.as_ref(),
686 password.as_ref()
687 ));
688 self.config
689 .headers
690 .insert("Authorization".to_string(), format!("Basic {}", encoded));
691 self
692 }
693
694 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
708 self.config.headers.insert(name.into(), value.into());
709 self
710 }
711
712 pub fn disable_session_recovery(mut self) -> Self {
719 self.config.session_recovery = false;
720 self
721 }
722
723 #[cfg(feature = "oauth-client")]
753 pub fn with_token_provider(mut self, provider: impl TokenProvider) -> Self {
754 self.token_provider = Some(Arc::new(provider));
755 self.scope_escalation = None;
756 self
757 }
758
759 #[cfg(feature = "oauth-client")]
772 pub fn with_scope_aware_token_provider<P>(
773 mut self,
774 provider: P,
775 config: OAuthScopeEscalationConfig,
776 ) -> Self
777 where
778 P: TokenProvider + OAuthScopeEscalationHandler,
779 {
780 let provider = Arc::new(provider);
781 self.token_provider = Some(provider.clone());
782 self.scope_escalation = Some(ScopeEscalationRuntime::new(provider, config));
783 self
784 }
785
786 fn outgoing_custom_headers(&self, parsed: &serde_json::Value) -> Vec<(String, String)> {
787 if parsed.get("method").and_then(serde_json::Value::as_str) != Some("tools/call") {
788 return Vec::new();
789 }
790 let Some(params) = parsed.get("params") else {
791 return Vec::new();
792 };
793 let Some(name) = params.get("name").and_then(serde_json::Value::as_str) else {
794 return Vec::new();
795 };
796 let Some(mappings) = self.tool_header_mappings.get(name) else {
797 return Vec::new();
798 };
799 let arguments = params.get("arguments").unwrap_or(&serde_json::Value::Null);
800
801 mappings
802 .iter()
803 .filter_map(|mapping| {
804 let value = value_at_property_path(arguments, &mapping.property_path)?;
805 if value.is_null() {
806 return None;
807 }
808 let rendered = json_value_to_header_string(value)?;
809 Some((
810 format!("{MCP_PARAM_HEADER_PREFIX}{}", mapping.suffix),
811 encode_header_value(&rendered),
812 ))
813 })
814 .collect()
815 }
816
817 fn normalize_incoming_message(&mut self, message: String) -> String {
818 if self.protocol_version.as_deref() != Some(crate::protocol::PROTOCOL_VERSION_2026_07_28) {
819 return message;
820 }
821 let Ok(mut parsed) = serde_json::from_str::<serde_json::Value>(&message) else {
822 return message;
823 };
824 if parsed.get("method").and_then(serde_json::Value::as_str)
827 == Some("notifications/tools/list_changed")
828 {
829 self.tool_header_mappings.clear();
830 return message;
831 }
832 let later_page = parsed
835 .get("id")
836 .and_then(|id| {
837 let index = self
838 .pending_tools_lists
839 .iter()
840 .position(|(pending, _)| json_request_ids_match(pending, id))?;
841 Some(self.pending_tools_lists.remove(index).1)
842 })
843 .unwrap_or(false);
844 let Some(tools) = parsed
845 .get_mut("result")
846 .and_then(|result| result.get_mut("tools"))
847 .and_then(serde_json::Value::as_array_mut)
848 else {
849 return message;
850 };
851
852 if !later_page {
856 self.tool_header_mappings.clear();
857 }
858 tools.retain(|tool| {
859 let Some(name) = tool.get("name").and_then(serde_json::Value::as_str) else {
860 return false;
861 };
862 let Some(schema) = tool.get("inputSchema") else {
863 return false;
864 };
865 match custom_header_mappings(schema) {
866 Ok(mappings) => {
867 self.tool_header_mappings.insert(name.to_string(), mappings);
868 true
869 }
870 Err(error) => {
871 tracing::warn!(tool = %name, %error, "Excluding tool with invalid x-mcp-header annotations");
872 false
873 }
874 }
875 });
876
877 parsed.to_string()
878 }
879
880 async fn queue_incoming(&self, frame: String) -> Result<()> {
882 self.incoming_tx
883 .send(frame)
884 .await
885 .map_err(|_| Error::Transport("Internal channel closed".to_string()))
886 }
887
888 fn start_sse_stream(&mut self) {
890 let url = self.url.clone();
891 let client = self.client.clone();
892 let session_id = self.session_id.clone().unwrap();
893 let protocol_version = self.protocol_version.clone();
894 let tx = self.incoming_tx.clone();
895 let last_event_id = self.last_event_id.clone();
896 let sse_retry_delay = self.sse_retry_delay.clone();
897 let reconnect_signal = self.sse_reconnect_signal.clone();
898 let connected = self.connected.clone();
899 let config = self.config.clone();
900 #[cfg(feature = "oauth-client")]
901 let token_provider = self.token_provider.clone();
902
903 self.sse_task = Some(tokio::spawn(async move {
904 sse_stream_loop(SseLoopParams {
905 url,
906 client,
907 session_id,
908 protocol_version,
909 tx,
910 last_event_id,
911 sse_retry_delay,
912 reconnect_signal,
913 connected,
914 config,
915 #[cfg(feature = "oauth-client")]
916 token_provider,
917 })
918 .await;
919 }));
920 }
921}
922
923fn custom_header_mappings(
924 schema: &serde_json::Value,
925) -> std::result::Result<Vec<CustomHeaderMapping>, String> {
926 fn annotation_count(value: &serde_json::Value) -> usize {
927 match value {
928 serde_json::Value::Object(object) => {
929 usize::from(object.contains_key("x-mcp-header"))
930 + object.values().map(annotation_count).sum::<usize>()
931 }
932 serde_json::Value::Array(values) => values.iter().map(annotation_count).sum::<usize>(),
933 _ => 0,
934 }
935 }
936
937 fn is_tchar(byte: u8) -> bool {
938 byte.is_ascii_alphanumeric()
939 || matches!(
940 byte,
941 b'!' | b'#'
942 | b'$'
943 | b'%'
944 | b'&'
945 | b'\''
946 | b'*'
947 | b'+'
948 | b'-'
949 | b'.'
950 | b'^'
951 | b'_'
952 | b'`'
953 | b'|'
954 | b'~'
955 )
956 }
957
958 fn primitive_header_type(schema: &serde_json::Value) -> bool {
960 match schema.get("type") {
961 Some(serde_json::Value::String(kind)) => {
962 matches!(kind.as_str(), "string" | "integer" | "boolean")
963 }
964 Some(serde_json::Value::Array(kinds)) => {
965 let mut primitive = false;
966 for kind in kinds {
967 match kind.as_str() {
968 Some("string" | "integer" | "boolean") if !primitive => {
969 primitive = true;
970 }
971 Some("null") => {}
972 _ => return false,
973 }
974 }
975 primitive
976 }
977 _ => false,
978 }
979 }
980
981 fn walk(
982 schema: &serde_json::Value,
983 path: &mut Vec<String>,
984 seen: &mut std::collections::HashSet<String>,
985 mappings: &mut Vec<CustomHeaderMapping>,
986 ) -> std::result::Result<(), String> {
987 let Some(properties) = schema
988 .get("properties")
989 .and_then(serde_json::Value::as_object)
990 else {
991 return Ok(());
992 };
993 for (property_name, property_schema) in properties {
994 path.push(property_name.clone());
995 if let Some(annotation) = property_schema.get("x-mcp-header") {
996 let suffix = annotation
997 .as_str()
998 .ok_or_else(|| format!("annotation at {} is not a string", path.join(".")))?;
999 if suffix.is_empty() || !suffix.bytes().all(is_tchar) {
1000 return Err(format!(
1001 "invalid header suffix {suffix:?} at {}",
1002 path.join(".")
1003 ));
1004 }
1005 if !primitive_header_type(property_schema) {
1006 return Err(format!(
1007 "annotation at {} is not on a primitive property",
1008 path.join(".")
1009 ));
1010 }
1011 if !seen.insert(suffix.to_ascii_lowercase()) {
1012 return Err(format!("duplicate header suffix {suffix:?}"));
1013 }
1014 mappings.push(CustomHeaderMapping {
1015 suffix: suffix.to_string(),
1016 property_path: path.clone(),
1017 });
1018 }
1019 walk(property_schema, path, seen, mappings)?;
1020 path.pop();
1021 }
1022 Ok(())
1023 }
1024
1025 let mut mappings = Vec::new();
1026 walk(
1027 schema,
1028 &mut Vec::new(),
1029 &mut std::collections::HashSet::new(),
1030 &mut mappings,
1031 )?;
1032 if annotation_count(schema) != mappings.len() {
1033 return Err(
1034 "x-mcp-header annotation is not statically reachable through properties".to_string(),
1035 );
1036 }
1037 Ok(mappings)
1038}
1039
1040fn value_at_property_path<'a>(
1041 root: &'a serde_json::Value,
1042 path: &[String],
1043) -> Option<&'a serde_json::Value> {
1044 path.iter().try_fold(root, |value, key| value.get(key))
1045}
1046
1047fn json_value_to_header_string(value: &serde_json::Value) -> Option<String> {
1048 match value {
1049 serde_json::Value::String(value) => Some(value.clone()),
1050 serde_json::Value::Number(value) => Some(value.to_string()),
1051 serde_json::Value::Bool(value) => Some(value.to_string()),
1052 serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
1053 None
1054 }
1055 }
1056}
1057
1058fn encode_header_value(value: &str) -> String {
1059 let unsafe_for_header =
1060 value.trim() != value || value.bytes().any(|byte| !(0x20..=0x7e).contains(&byte));
1061 if unsafe_for_header {
1062 format!(
1063 "{BASE64_SENTINEL_PREFIX}{}{BASE64_SENTINEL_SUFFIX}",
1064 base64::engine::general_purpose::STANDARD.encode(value)
1065 )
1066 } else {
1067 value.to_string()
1068 }
1069}
1070
1071#[cfg(feature = "oauth-client")]
1072fn bearer_headers(token: &str) -> std::result::Result<reqwest::header::HeaderMap, String> {
1073 let value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
1074 .map_err(|_| "token provider returned an invalid bearer token".to_string())?;
1075 let mut headers = reqwest::header::HeaderMap::new();
1076 headers.insert(reqwest::header::AUTHORIZATION, value);
1077 Ok(headers)
1081}
1082
1083fn is_jsonrpc_error_response(value: &serde_json::Value) -> bool {
1084 value.get("error").is_some_and(serde_json::Value::is_object)
1085 && value.pointer("/error/code").is_some()
1086 && value.pointer("/error/message").is_some()
1087}
1088
1089struct HttpRequestSendError {
1090 message: String,
1091 connection_failed: bool,
1092}
1093
1094fn request_error_message(error: &reqwest::Error) -> String {
1097 let category = if error.is_timeout() {
1098 "timeout"
1099 } else if error.is_redirect() {
1100 "redirect rejected (check redirect policy)"
1101 } else if error.is_connect() {
1102 "connection failed"
1103 } else if error.is_builder() {
1104 "invalid request configuration"
1105 } else if error.is_body() {
1106 "response body I/O failed"
1107 } else if error.is_decode() {
1108 "response decoding failed"
1109 } else {
1110 "request failed"
1111 };
1112 match error.status() {
1113 Some(status) => format!("HTTP request failed: {category} (HTTP {status})"),
1114 None => format!("HTTP request failed: {category}"),
1115 }
1116}
1117
1118impl HttpRequestSendError {
1119 fn request(error: reqwest::Error) -> Self {
1120 Self {
1121 message: request_error_message(&error),
1122 connection_failed: true,
1123 }
1124 }
1125
1126 #[cfg(feature = "oauth-client")]
1127 fn oauth(_error: OAuthClientError) -> Self {
1128 Self {
1129 message: "OAuth token acquisition failed".to_string(),
1130 connection_failed: false,
1131 }
1132 }
1133}
1134
1135#[cfg(not(feature = "oauth-client"))]
1136async fn send_http_request(
1137 request: reqwest::RequestBuilder,
1138 resource: &str,
1139 operation: &str,
1140) -> std::result::Result<reqwest::Response, HttpRequestSendError> {
1141 let _ = (resource, operation);
1142 request.send().await.map_err(HttpRequestSendError::request)
1143}
1144
1145#[cfg(feature = "oauth-client")]
1146async fn send_http_request(
1147 mut request: reqwest::RequestBuilder,
1148 resource: &str,
1149 operation: &str,
1150 token_provider: Option<Arc<dyn TokenProvider>>,
1151 scope_escalation: Option<ScopeEscalationRuntime>,
1152 initial_scope_revision: usize,
1153) -> std::result::Result<reqwest::Response, HttpRequestSendError> {
1154 let mut observed_revision = initial_scope_revision;
1155 let mut attempts = 0;
1156 let mut reauthenticated = false;
1162
1163 loop {
1164 let retry_request = request.try_clone();
1165
1166 let response = request
1167 .send()
1168 .await
1169 .map_err(HttpRequestSendError::request)?;
1170
1171 if response.status() == reqwest::StatusCode::UNAUTHORIZED
1176 && !reauthenticated
1177 && bearer_challenge(response.headers()).is_some()
1178 && let Some(provider) = token_provider.as_ref()
1179 && let Some(reauth_request) = retry_request.as_ref().and_then(|r| r.try_clone())
1182 {
1183 reauthenticated = true;
1184 provider.invalidate().await;
1185 let token = provider
1186 .get_token()
1187 .await
1188 .map_err(HttpRequestSendError::oauth)?;
1189 let headers = bearer_headers(&token).map_err(|message| {
1190 HttpRequestSendError::oauth(OAuthClientError::ScopeEscalation(message))
1191 })?;
1192 request = reauth_request.headers(headers);
1193 continue;
1194 }
1195
1196 let challenge = if response.status() == reqwest::StatusCode::FORBIDDEN {
1197 scope_challenge(response.headers())
1198 } else {
1199 None
1200 };
1201 let Some(challenge) = challenge else {
1202 return Ok(response);
1203 };
1204 let (Some(runtime), Some(provider), Some(mut retry_request)) = (
1205 scope_escalation.as_ref(),
1206 token_provider.as_ref(),
1207 retry_request,
1208 ) else {
1209 return Ok(response);
1210 };
1211 if attempts >= runtime.max_attempts {
1212 return Ok(response);
1213 }
1214
1215 attempts += 1;
1216 let decision = runtime
1217 .respond_to_challenge(challenge, resource, operation, attempts, observed_revision)
1218 .await
1219 .map_err(HttpRequestSendError::oauth)?;
1220 observed_revision = decision.revision;
1221
1222 let token = provider
1223 .get_token()
1224 .await
1225 .map_err(HttpRequestSendError::oauth)?;
1226 let headers = bearer_headers(&token).map_err(|message| {
1227 HttpRequestSendError::oauth(OAuthClientError::ScopeEscalation(message))
1228 })?;
1229 retry_request = retry_request.headers(headers);
1230 request = retry_request;
1231 }
1232}
1233
1234#[cfg(feature = "oauth-client")]
1235fn scope_challenge(headers: &reqwest::header::HeaderMap) -> Option<OAuthScopeChallenge> {
1236 headers
1237 .get_all(reqwest::header::WWW_AUTHENTICATE)
1238 .iter()
1239 .filter_map(|value| value.to_str().ok())
1240 .find_map(OAuthScopeChallenge::from_www_authenticate)
1241}
1242
1243#[cfg(feature = "oauth-client")]
1253fn bearer_challenge(headers: &reqwest::header::HeaderMap) -> Option<OAuthBearerChallenge> {
1254 headers
1255 .get_all(reqwest::header::WWW_AUTHENTICATE)
1256 .iter()
1257 .filter_map(|value| value.to_str().ok())
1258 .find_map(OAuthBearerChallenge::from_www_authenticate)
1259}
1260
1261fn http_status_error(status: reqwest::StatusCode, headers: &reqwest::header::HeaderMap) -> String {
1262 #[cfg(feature = "oauth-client")]
1263 if scope_challenge(headers).is_some() {
1264 return format!("server returned HTTP {status}: insufficient_scope");
1265 }
1266
1267 #[cfg(feature = "oauth-client")]
1272 if let Some(challenge) = bearer_challenge(headers) {
1273 let detail = match challenge.error.as_deref() {
1274 Some("invalid_request") => "invalid_request",
1275 Some("invalid_token") => "invalid_token",
1276 Some("insufficient_scope") => "insufficient_scope",
1277 _ => "authentication rejected",
1278 };
1279 return format!("server returned HTTP {status}: {detail}");
1280 }
1281
1282 #[cfg(not(feature = "oauth-client"))]
1283 let _ = headers;
1284 if matches!(
1288 status,
1289 reqwest::StatusCode::MOVED_PERMANENTLY
1290 | reqwest::StatusCode::FOUND
1291 | reqwest::StatusCode::SEE_OTHER
1292 ) {
1293 return format!(
1294 "server returned HTTP {status}: redirect not followed (only 307 and 308 keep \
1295 the request body; use the final URL)"
1296 );
1297 }
1298 format!("server returned HTTP {status}")
1299}
1300
1301enum BoundedBodyError {
1303 TooLarge(String),
1307 Io(reqwest::Error),
1309}
1310
1311async fn read_bounded_body(
1320 response: reqwest::Response,
1321 limit: usize,
1322) -> std::result::Result<String, BoundedBodyError> {
1323 if let Some(len) = response.content_length()
1324 && len > limit as u64
1325 {
1326 return Err(BoundedBodyError::TooLarge(format!(
1327 "response body of {len} bytes exceeds the {limit}-byte limit"
1328 )));
1329 }
1330
1331 use futures::StreamExt;
1332 let mut stream = response.bytes_stream();
1333 let mut buf = Vec::new();
1334 while let Some(chunk) = stream.next().await {
1335 let chunk = chunk.map_err(BoundedBodyError::Io)?;
1336 buf.extend_from_slice(&chunk);
1337 if buf.len() > limit {
1338 return Err(BoundedBodyError::TooLarge(format!(
1339 "response body exceeds the {limit}-byte limit"
1340 )));
1341 }
1342 }
1343 Ok(String::from_utf8_lossy(&buf).into_owned())
1344}
1345
1346const MAX_ERROR_BODY_SIZE: usize = 64 * 1024;
1352
1353async fn read_error_body(response: reqwest::Response) -> String {
1356 use futures::StreamExt;
1357 let mut stream = response.bytes_stream();
1358 let mut buf = Vec::new();
1359 while let Some(chunk) = stream.next().await {
1360 let Ok(chunk) = chunk else { break };
1361 buf.extend_from_slice(&chunk);
1362 if buf.len() > MAX_ERROR_BODY_SIZE {
1363 buf.truncate(MAX_ERROR_BODY_SIZE);
1364 break;
1365 }
1366 }
1367 String::from_utf8_lossy(&buf).into_owned()
1368}
1369
1370fn operation_label(parsed: Option<&serde_json::Value>) -> String {
1371 let Some(method) = parsed
1372 .and_then(|value| value.get("method"))
1373 .and_then(serde_json::Value::as_str)
1374 else {
1375 return "unknown".to_string();
1376 };
1377 let target = match method {
1378 "tools/call" | "prompts/get" => parsed
1379 .and_then(|value| value.pointer("/params/name"))
1380 .and_then(serde_json::Value::as_str),
1381 "resources/read" => parsed
1382 .and_then(|value| value.pointer("/params/uri"))
1383 .and_then(serde_json::Value::as_str),
1384 "tasks/get" | "tasks/update" | "tasks/cancel" => parsed
1385 .and_then(|value| value.pointer("/params/taskId"))
1386 .and_then(serde_json::Value::as_str),
1387 _ => None,
1388 };
1389 match target {
1390 Some(target) => format!("{method}:{target}"),
1391 None => method.to_string(),
1392 }
1393}
1394
1395#[async_trait]
1396impl ClientTransport for HttpClientTransport {
1397 async fn send(&mut self, message: &str) -> Result<()> {
1398 if !self.connected.load(Ordering::Acquire) {
1399 return Err(Error::Transport("Transport closed".to_string()));
1400 }
1401
1402 let parsed_message = serde_json::from_str::<serde_json::Value>(message).ok();
1409 let is_notification = parsed_message
1410 .as_ref()
1411 .map(|v| v.get("id").is_none())
1412 .unwrap_or(false);
1413 let method = parsed_message
1414 .as_ref()
1415 .and_then(|value| value.get("method"))
1416 .and_then(serde_json::Value::as_str);
1417 let reply_id = parsed_message
1421 .as_ref()
1422 .filter(|value| value.get("method").is_some())
1423 .and_then(|value| value.get("id"))
1424 .cloned();
1425 let operation = operation_label(parsed_message.as_ref());
1426 if method == Some("initialize") {
1427 self.initialize_request_id = reply_id
1428 .clone()
1429 .and_then(|value| serde_json::from_value(value).ok());
1430 }
1431 let outbound_version = parsed_message
1432 .as_ref()
1433 .and_then(|value| {
1434 value.pointer("/params/_meta/io.modelcontextprotocol~1protocolVersion")
1435 })
1436 .and_then(serde_json::Value::as_str)
1437 .map(str::to_string);
1438 let is_modern_request =
1439 outbound_version.as_deref() == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28);
1440 if is_modern_request {
1441 self.protocol_version = outbound_version.clone();
1442 self.session_id = None;
1445 }
1446 if is_modern_request
1447 && method == Some("tools/list")
1448 && let Some(id) = reply_id.as_ref()
1449 {
1450 let has_cursor = parsed_message
1451 .as_ref()
1452 .and_then(|value| value.pointer("/params/cursor"))
1453 .is_some_and(|cursor| !cursor.is_null());
1454 if self.pending_tools_lists.len() >= MAX_PENDING_TOOLS_LISTS {
1455 self.pending_tools_lists.remove(0);
1456 }
1457 self.pending_tools_lists.push((id.clone(), has_cursor));
1458 }
1459 let timeout = if is_notification {
1460 self.config
1461 .notification_timeout
1462 .min(self.config.request_timeout)
1463 } else {
1464 self.config.request_timeout
1465 };
1466
1467 let mut request = self
1469 .client
1470 .post(&self.url)
1471 .header("Content-Type", "application/json")
1472 .header("Accept", "application/json, text/event-stream");
1473 if method != Some("subscriptions/listen") {
1477 request = request.timeout(timeout);
1478 }
1479
1480 if !is_modern_request && let Some(ref session_id) = self.session_id {
1481 request = request.header("mcp-session-id", session_id);
1482 }
1483
1484 if let Some(version) = outbound_version.as_ref().or(self.protocol_version.as_ref()) {
1485 request = request.header("mcp-protocol-version", version);
1486 }
1487
1488 if let Some(method) = method {
1489 request = request.header(MCP_METHOD_HEADER, method);
1490 let name = match method {
1491 "tools/call" | "prompts/get" => parsed_message
1492 .as_ref()
1493 .and_then(|value| value.pointer("/params/name"))
1494 .and_then(serde_json::Value::as_str),
1495 "resources/read" => parsed_message
1496 .as_ref()
1497 .and_then(|value| value.pointer("/params/uri"))
1498 .and_then(serde_json::Value::as_str),
1499 "tasks/get" | "tasks/update" | "tasks/cancel" => parsed_message
1500 .as_ref()
1501 .and_then(|value| value.pointer("/params/taskId"))
1502 .and_then(serde_json::Value::as_str),
1503 _ => None,
1504 };
1505 if let Some(name) = name {
1506 request = request.header(MCP_NAME_HEADER, name);
1507 }
1508 }
1509
1510 if let Some(parsed) = parsed_message.as_ref() {
1511 for (name, value) in self.outgoing_custom_headers(parsed) {
1512 request = request.header(name, value);
1513 }
1514 }
1515
1516 for (key, value) in &self.config.headers {
1517 request = request.header(key.as_str(), value.as_str());
1518 }
1519
1520 #[cfg(feature = "oauth-client")]
1521 let initial_scope_revision = match &self.scope_escalation {
1522 Some(runtime) => runtime.state.lock().await.revision,
1523 None => 0,
1524 };
1525
1526 #[cfg(feature = "oauth-client")]
1528 if let Some(ref provider) = self.token_provider {
1529 let token = provider
1530 .get_token()
1531 .await
1532 .map_err(|_| Error::Transport("Token provider failed".to_string()))?;
1533 request = request.headers(bearer_headers(&token).map_err(Error::Transport)?);
1534 }
1535
1536 let request = request.body(message.to_string());
1537
1538 if !is_notification && (self.session_id.is_some() || is_modern_request) {
1549 let tx = self.incoming_tx.clone();
1550 let request_id = reply_id
1563 .clone()
1564 .and_then(|value| serde_json::from_value(value).ok());
1565 let is_subscription = method == Some("subscriptions/listen");
1566 let session_expiry_on_404 = !is_modern_request
1571 && self.session_id.is_some()
1572 && self.config.session_recovery
1573 && reply_id.is_some();
1574 let reply_id = reply_id.clone();
1575 let connected = self.connected.clone();
1576 let last_event_id = self.last_event_id.clone();
1577 let sse_retry_delay = self.sse_retry_delay.clone();
1578 let sse_reconnect_signal = self.sse_reconnect_signal.clone();
1579 let max_sse_event_size = self.config.max_sse_event_size;
1580 let max_response_body_size = self.config.max_response_body_size;
1581 let request_resource = self.url.clone();
1582 #[cfg(feature = "oauth-client")]
1583 let token_provider = self.token_provider.clone();
1584 #[cfg(feature = "oauth-client")]
1585 let scope_escalation = self.scope_escalation.clone();
1586 self.request_tasks.retain(|_, task| !task.is_finished());
1587 let task = tokio::spawn(async move {
1588 let response_result = send_http_request(
1589 request,
1590 &request_resource,
1591 &operation,
1592 #[cfg(feature = "oauth-client")]
1593 token_provider,
1594 #[cfg(feature = "oauth-client")]
1595 scope_escalation,
1596 #[cfg(feature = "oauth-client")]
1597 initial_scope_revision,
1598 )
1599 .await;
1600 let response = match response_result {
1601 Ok(r) => r,
1602 Err(e) => {
1603 let connection_failed = e.connection_failed;
1604 tracing::error!(error = %e.message, "Background HTTP request failed");
1605 if let Some(id) = &reply_id {
1606 let _ = tx.send(transport_error_frame(id, &e.message)).await;
1607 }
1608 if connection_failed {
1609 connected.store(false, Ordering::Release);
1610 }
1611 return;
1612 }
1613 };
1614
1615 let status = response.status();
1616
1617 if status == reqwest::StatusCode::ACCEPTED {
1620 if let Some(id) = &reply_id {
1621 let _ = tx
1622 .send(transport_error_frame(id, ACCEPTED_WITHOUT_RESPONSE))
1623 .await;
1624 }
1625 return;
1626 }
1627
1628 if !status.is_success() {
1629 let status_error = http_status_error(status, response.headers());
1630 let retry_after = retry_after_header(response.headers());
1631 let www_authenticate = www_authenticate_header(response.headers());
1632 let body = read_error_body(response).await;
1633
1634 if status == reqwest::StatusCode::NOT_FOUND
1641 && session_expiry_on_404
1642 && let Some(id) = &reply_id
1643 {
1644 tracing::info!("HTTP 404 on a session request, signalling session expiry");
1645 let _ = tx.send(session_expired_frame(id)).await;
1646 return;
1647 }
1648
1649 if !body.is_empty()
1661 && let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&body)
1662 && is_jsonrpc_error_response(&v)
1663 {
1664 let is_session_signal =
1665 v.pointer("/error/code").and_then(|c| c.as_i64()) == Some(-32005);
1666 if reply_id.is_none() && !is_session_signal {
1667 tracing::warn!(status = %status, "reply POST returned a JSON-RPC error body");
1668 return;
1669 }
1670 if !is_session_signal
1671 && let Some(id) = &reply_id
1672 && !v
1673 .get("id")
1674 .is_some_and(|actual| json_request_ids_match(actual, id))
1675 {
1676 v["id"] = id.clone();
1677 }
1678 if !is_session_signal && status_annotates_jsonrpc_error(status) {
1679 merge_status_into_error(
1680 &mut v,
1681 status_error_data(status, &retry_after, &www_authenticate),
1682 );
1683 }
1684 let _ = tx.send(v.to_string()).await;
1685 return;
1686 }
1687
1688 tracing::error!(status = %status, "HTTP error from server");
1689 if let Some(id) = &reply_id {
1690 let _ = tx
1691 .send(transport_error_frame_with_data(
1692 id,
1693 &status_error,
1694 status_error_data(status, &retry_after, &www_authenticate),
1695 ))
1696 .await;
1697 }
1698 if status_ends_transport(status) {
1703 connected.store(false, Ordering::Release);
1704 }
1705 return;
1706 }
1707
1708 let is_sse = response
1710 .headers()
1711 .get("content-type")
1712 .and_then(|v| v.to_str().ok())
1713 .is_some_and(|ct| ct.contains("text/event-stream"));
1714
1715 if is_sse {
1716 pump_post_sse(
1717 response,
1718 PostSseContext {
1719 tx,
1720 reply_id,
1721 is_subscription,
1722 is_modern_request,
1723 connected,
1724 last_event_id,
1725 sse_retry_delay,
1726 sse_reconnect_signal,
1727 max_sse_event_size,
1728 },
1729 )
1730 .await;
1731 } else {
1732 match read_bounded_body(response, max_response_body_size).await {
1734 Ok(body) if !body.is_empty() => {
1735 let msgs = extract_json_messages(&body);
1736 if msgs.is_empty() {
1737 if let Some(id) = &reply_id {
1742 let _ = tx
1743 .send(transport_error_frame(
1744 id,
1745 "server returned an unparseable response body",
1746 ))
1747 .await;
1748 } else {
1749 tracing::warn!(
1750 "reply POST returned an unparseable response body"
1751 );
1752 }
1753 } else {
1754 let answered = reply_id
1757 .as_ref()
1758 .is_none_or(|id| frames_answer_request(&msgs, id));
1759 for msg in msgs {
1760 let _ = tx.send(msg).await;
1761 }
1762 if !answered && let Some(id) = &reply_id {
1763 let _ = tx
1764 .send(transport_error_frame(id, NO_RESPONSE_IN_BODY))
1765 .await;
1766 }
1767 }
1768 }
1769 Ok(_) => {
1770 if let Some(id) = &reply_id {
1775 let _ = tx
1776 .send(transport_error_frame(
1777 id,
1778 "server returned an empty response body",
1779 ))
1780 .await;
1781 } else {
1782 tracing::warn!("reply POST returned an empty response body");
1783 }
1784 }
1785 Err(BoundedBodyError::TooLarge(message)) => {
1786 tracing::error!(
1791 error = %message,
1792 "Background HTTP response body exceeded the size limit"
1793 );
1794 if let Some(id) = &reply_id {
1795 let _ = tx.send(transport_error_frame(id, &message)).await;
1796 }
1797 }
1798 Err(BoundedBodyError::Io(e)) => {
1799 let message = request_error_message(&e);
1800 tracing::error!(error = %message, "Failed to read response body");
1801 if let Some(id) = &reply_id {
1802 let _ = tx
1803 .send(transport_error_frame(
1804 id,
1805 &format!("failed to read response body: {message}"),
1806 ))
1807 .await;
1808 }
1809 connected.store(false, Ordering::Release);
1810 }
1811 }
1812 }
1813 });
1814 if let Some(request_id) = request_id {
1815 self.request_tasks.insert(request_id, task);
1816 }
1817 return Ok(());
1818 }
1819
1820 let response = send_http_request(
1824 request,
1825 &self.url,
1826 &operation,
1827 #[cfg(feature = "oauth-client")]
1828 self.token_provider.clone(),
1829 #[cfg(feature = "oauth-client")]
1830 self.scope_escalation.clone(),
1831 #[cfg(feature = "oauth-client")]
1832 initial_scope_revision,
1833 )
1834 .await
1835 .map_err(|e| Error::Transport(e.message))?;
1836
1837 let status = response.status();
1838
1839 let new_session_id = response
1841 .headers()
1842 .get("mcp-session-id")
1843 .and_then(|v| v.to_str().ok())
1844 .map(|s| s.to_string());
1845 let new_protocol_version = response
1846 .headers()
1847 .get("mcp-protocol-version")
1848 .and_then(|v| v.to_str().ok())
1849 .map(|s| s.to_string());
1850
1851 if status == reqwest::StatusCode::ACCEPTED {
1853 if !is_modern_request && let Some(sid) = new_session_id {
1855 self.session_id = Some(sid);
1856 }
1857 if let Some(pv) = new_protocol_version {
1858 self.protocol_version = Some(pv);
1859 }
1860 if let Some(id) = &reply_id {
1862 self.queue_incoming(transport_error_frame(id, ACCEPTED_WITHOUT_RESPONSE))
1863 .await?;
1864 }
1865 return Ok(());
1866 }
1867
1868 if !status.is_success() {
1869 let status_error = http_status_error(status, response.headers());
1870 let body = read_error_body(response).await;
1871 if is_modern_request
1872 && let Ok(mut error) = serde_json::from_str::<serde_json::Value>(&body)
1873 && is_jsonrpc_error_response(&error)
1874 {
1875 if let Some(id) = parsed_message.as_ref().and_then(|value| value.get("id"))
1876 && !error
1877 .get("id")
1878 .is_some_and(|actual| json_request_ids_match(actual, id))
1879 {
1880 error["id"] = id.clone();
1881 }
1882 self.queue_incoming(error.to_string()).await?;
1883 return Ok(());
1884 }
1885 if status == reqwest::StatusCode::NOT_FOUND
1890 && self.config.session_recovery
1891 && self.session_id.is_some()
1892 {
1893 return Err(Error::SessionExpired);
1894 }
1895 if status == reqwest::StatusCode::NOT_FOUND && self.session_id.is_none() {
1896 return Err(Error::Transport(
1897 "HTTP 404: MCP endpoint not found (check the endpoint path; \
1898 some servers serve MCP at the root, others at /mcp)"
1899 .to_string(),
1900 ));
1901 }
1902 return Err(Error::Transport(status_error));
1903 }
1904
1905 if !is_modern_request && let Some(sid) = new_session_id {
1907 let is_new_session = self.session_id.is_none();
1908 self.session_id = Some(sid);
1909
1910 if is_new_session && self.config.auto_sse {
1911 self.start_sse_stream();
1912 }
1913 }
1914 if let Some(pv) = new_protocol_version {
1915 self.protocol_version = Some(pv);
1916 }
1917
1918 let is_sse = response
1924 .headers()
1925 .get("content-type")
1926 .and_then(|v| v.to_str().ok())
1927 .is_some_and(|ct| ct.contains("text/event-stream"));
1928 if is_sse && reply_id.is_some() {
1929 let request_id = reply_id
1930 .clone()
1931 .and_then(|value| serde_json::from_value(value).ok());
1932 self.request_tasks.retain(|_, task| !task.is_finished());
1933 let task = tokio::spawn(pump_post_sse(
1934 response,
1935 PostSseContext {
1936 tx: self.incoming_tx.clone(),
1937 reply_id,
1938 is_subscription: false,
1939 is_modern_request,
1940 connected: self.connected.clone(),
1941 last_event_id: self.last_event_id.clone(),
1942 sse_retry_delay: self.sse_retry_delay.clone(),
1943 sse_reconnect_signal: self.sse_reconnect_signal.clone(),
1944 max_sse_event_size: self.config.max_sse_event_size,
1945 },
1946 ));
1947 if let Some(request_id) = request_id {
1948 self.request_tasks.insert(request_id, task);
1949 }
1950 return Ok(());
1951 }
1952
1953 let body = match read_bounded_body(response, self.config.max_response_body_size).await {
1955 Ok(body) => body,
1956 Err(BoundedBodyError::TooLarge(message)) => {
1957 if let Some(id) = &reply_id {
1962 self.queue_incoming(transport_error_frame(id, &message))
1963 .await?;
1964 }
1965 return Ok(());
1966 }
1967 Err(BoundedBodyError::Io(e)) => {
1968 return Err(Error::Transport(format!(
1969 "Failed to read response: {}",
1970 request_error_message(&e)
1971 )));
1972 }
1973 };
1974
1975 let msgs = extract_json_messages(&body);
1976 let answered = reply_id
1977 .as_ref()
1978 .is_none_or(|id| frames_answer_request(&msgs, id));
1979 let no_reply = if msgs.is_empty() {
1980 "server returned an empty response body"
1981 } else {
1982 NO_RESPONSE_IN_BODY
1983 };
1984 for msg in msgs {
1985 self.queue_incoming(msg).await?;
1986 }
1987 if !answered && let Some(id) = &reply_id {
1990 self.queue_incoming(transport_error_frame(id, no_reply))
1991 .await?;
1992 }
1993
1994 Ok(())
1995 }
1996
1997 async fn recv(&mut self) -> Result<Option<String>> {
1998 match self.incoming_rx.recv().await {
1999 Some(msg) => Ok(Some(self.normalize_incoming_message(msg))),
2004 None => {
2005 self.connected.store(false, Ordering::Release);
2006 Ok(None)
2007 }
2008 }
2009 }
2010
2011 fn is_connected(&self) -> bool {
2012 self.connected.load(Ordering::Acquire)
2013 }
2014
2015 async fn close(&mut self) -> Result<()> {
2016 self.connected.store(false, Ordering::Release);
2017
2018 for (_, task) in self.request_tasks.drain() {
2019 task.abort();
2020 }
2021
2022 if let Some(task) = self.sse_task.take() {
2024 task.abort();
2025 }
2026
2027 if let Some(session_id) = self.session_id.take() {
2031 send_session_delete(SessionDeleteParams {
2032 client: self.client.clone(),
2033 url: self.url.clone(),
2034 session_id,
2035 headers: self.config.headers.clone(),
2036 #[cfg(feature = "oauth-client")]
2037 token_provider: self.token_provider.clone(),
2038 })
2039 .await;
2040 }
2041
2042 Ok(())
2043 }
2044
2045 async fn reset_session(&mut self) {
2046 tracing::info!("Resetting session for re-initialization");
2047
2048 for (_, task) in self.request_tasks.drain() {
2049 task.abort();
2050 }
2051
2052 if let Some(task) = self.sse_task.take() {
2054 task.abort();
2055 }
2056
2057 self.session_id = None;
2059 self.protocol_version = None;
2060 *self.last_event_id.write().await = None;
2061 *self.sse_retry_delay.write().await = None;
2062
2063 while self.incoming_rx.try_recv().is_ok() {}
2065 }
2066
2067 async fn end_session(&mut self) {
2068 if let Some(session_id) = self.session_id.clone() {
2069 send_session_delete(SessionDeleteParams {
2070 client: self.client.clone(),
2071 url: self.url.clone(),
2072 session_id,
2073 headers: self.config.headers.clone(),
2074 #[cfg(feature = "oauth-client")]
2075 token_provider: self.token_provider.clone(),
2076 })
2077 .await;
2078 }
2079 self.reset_session().await;
2080 }
2081
2082 fn supports_session_recovery(&self) -> bool {
2083 self.config.session_recovery
2084 }
2085
2086 async fn cancel_request(&mut self, request_id: &RequestId) -> Result<()> {
2087 let mut in_flight = false;
2088 if let Some(task) = self.request_tasks.remove(request_id) {
2089 in_flight = !task.is_finished();
2093 task.abort();
2094 let _ = task.await;
2095 }
2096
2097 if in_flight
2102 && self.session_id.is_some()
2103 && self.initialize_request_id.as_ref() != Some(request_id)
2104 {
2105 let notification = serde_json::json!({
2106 "jsonrpc": "2.0",
2107 "method": "notifications/cancelled",
2108 "params": { "requestId": request_id },
2109 });
2110 if let Err(error) = self.send(¬ification.to_string()).await {
2114 tracing::warn!(%error, "Failed to send notifications/cancelled");
2115 }
2116 }
2117 Ok(())
2118 }
2119}
2120
2121impl Drop for HttpClientTransport {
2130 fn drop(&mut self) {
2131 for (_, task) in self.request_tasks.drain() {
2132 task.abort();
2133 }
2134 if let Some(task) = self.sse_task.take() {
2135 task.abort();
2136 }
2137
2138 let Some(session_id) = self.session_id.take() else {
2143 return;
2144 };
2145 let Ok(handle) = tokio::runtime::Handle::try_current() else {
2148 return;
2149 };
2150
2151 let params = SessionDeleteParams {
2152 client: self.client.clone(),
2153 url: self.url.clone(),
2154 session_id,
2155 headers: self.config.headers.clone(),
2156 #[cfg(feature = "oauth-client")]
2157 token_provider: self.token_provider.clone(),
2158 };
2159 handle.spawn(send_session_delete(params));
2160 }
2161}
2162
2163struct PostSseContext {
2169 tx: mpsc::Sender<String>,
2170 reply_id: Option<serde_json::Value>,
2173 is_subscription: bool,
2174 is_modern_request: bool,
2175 connected: Arc<AtomicBool>,
2176 last_event_id: Arc<RwLock<Option<String>>>,
2177 sse_retry_delay: Arc<RwLock<Option<Duration>>>,
2178 sse_reconnect_signal: Arc<Notify>,
2179 max_sse_event_size: usize,
2180}
2181
2182async fn pump_post_sse(response: reqwest::Response, ctx: PostSseContext) {
2190 let PostSseContext {
2191 tx,
2192 reply_id,
2193 is_subscription,
2194 is_modern_request,
2195 connected,
2196 last_event_id,
2197 sse_retry_delay,
2198 sse_reconnect_signal,
2199 max_sse_event_size,
2200 } = ctx;
2201
2202 let mut stream = response.bytes_stream();
2204 let mut parser = SseParser::with_limit(max_sse_event_size);
2205 let mut had_retry = false;
2206 let mut had_data = false;
2207 let mut subscription_acknowledged = false;
2208
2209 use futures::StreamExt;
2210 while let Some(result) = stream.next().await {
2211 match result {
2212 Ok(bytes) => {
2213 let text = String::from_utf8_lossy(&bytes);
2214 let events = match parser.feed(&text) {
2215 Ok(events) => events,
2216 Err(e) => {
2217 tracing::error!(error = %e, "POST SSE stream terminated");
2223 connected.store(false, Ordering::Release);
2224 return;
2225 }
2226 };
2227 for event in events {
2228 if let Some(ref id) = event.id {
2229 *last_event_id.write().await = Some(id.clone());
2230 }
2231 if let Some(retry_ms) = event.retry {
2232 *sse_retry_delay.write().await = Some(Duration::from_millis(retry_ms));
2233 had_retry = true;
2234 }
2235 if !event.data.is_empty() {
2236 had_data = true;
2237 let value = serde_json::from_str::<serde_json::Value>(&event.data);
2238 let value = match value {
2239 Ok(value) => value,
2240 Err(error) if is_subscription => {
2241 if let Some(id) = &reply_id {
2242 let _ = tx
2243 .send(transport_error_frame(
2244 id,
2245 &format!(
2246 "subscription stream returned invalid JSON: {error}"
2247 ),
2248 ))
2249 .await;
2250 }
2251 return;
2252 }
2253 Err(_) => {
2254 let _ = tx.send(event.data).await;
2255 continue;
2256 }
2257 };
2258 let is_terminal = value.get("id").zip(reply_id.as_ref()).is_some_and(
2259 |(actual, expected)| json_request_ids_match(actual, expected),
2260 ) && (value.get("result").is_some()
2261 || value.get("error").is_some());
2262
2263 if is_subscription {
2264 let violation = if is_terminal {
2265 if value.get("error").is_some() {
2266 None
2267 } else if !subscription_acknowledged {
2268 Some("subscriptions/listen completed before acknowledgment")
2269 } else if !value
2270 .pointer(
2271 "/result/_meta/io.modelcontextprotocol~1subscriptionId",
2272 )
2273 .zip(reply_id.as_ref())
2274 .is_some_and(|(actual, expected)| {
2275 json_request_ids_match(actual, expected)
2276 })
2277 {
2278 Some(
2279 "subscriptions/listen result carried a missing or mismatched subscription ID",
2280 )
2281 } else {
2282 None
2283 }
2284 } else if value.get("method").is_some() && value.get("id").is_none() {
2285 let correlated = value
2286 .pointer(
2287 "/params/_meta/io.modelcontextprotocol~1subscriptionId",
2288 )
2289 .zip(reply_id.as_ref())
2290 .is_some_and(|(actual, expected)| {
2291 json_request_ids_match(actual, expected)
2292 });
2293 let is_acknowledgment =
2294 value.get("method").and_then(serde_json::Value::as_str)
2295 == Some(notifications::SUBSCRIPTIONS_ACKNOWLEDGED);
2296 if !correlated {
2297 Some(
2298 "subscription notification carried a missing or mismatched subscription ID",
2299 )
2300 } else if !subscription_acknowledged && !is_acknowledgment {
2301 Some("subscription notification arrived before acknowledgment")
2302 } else if subscription_acknowledged && is_acknowledgment {
2303 Some("subscription stream sent a duplicate acknowledgment")
2304 } else {
2305 if is_acknowledgment {
2306 subscription_acknowledged = true;
2307 }
2308 None
2309 }
2310 } else {
2311 Some("subscription stream returned an unrelated JSON-RPC message")
2312 };
2313 if let Some(message) = violation {
2314 if let Some(id) = &reply_id {
2315 let _ = tx.send(transport_error_frame(id, message)).await;
2316 }
2317 return;
2318 }
2319 }
2320 let _ = tx.send(event.data).await;
2321 if is_terminal {
2322 return;
2326 }
2327 }
2328 }
2329 }
2330 Err(e) => {
2331 tracing::warn!(error = %request_error_message(&e), "POST SSE stream error");
2332 break;
2333 }
2334 }
2335 }
2336
2337 if !is_modern_request && had_retry && !had_data {
2342 sse_reconnect_signal.notify_one();
2343 } else {
2344 let reason = if had_data {
2350 "server closed the response stream before the final reply"
2351 } else {
2352 "server closed the response stream without a reply"
2353 };
2354 if let Some(id) = &reply_id {
2355 let _ = tx.send(transport_error_frame(id, reason)).await;
2356 } else {
2357 tracing::warn!(reason, "reply POST stream closed unexpectedly");
2358 }
2359 }
2360}
2361
2362struct SessionDeleteParams {
2370 client: reqwest::Client,
2371 url: String,
2372 session_id: String,
2373 headers: HashMap<String, String>,
2374 #[cfg(feature = "oauth-client")]
2375 token_provider: Option<Arc<dyn TokenProvider>>,
2376}
2377
2378async fn send_session_delete(params: SessionDeleteParams) {
2382 let SessionDeleteParams {
2383 client,
2384 url,
2385 session_id,
2386 headers,
2387 #[cfg(feature = "oauth-client")]
2388 token_provider,
2389 } = params;
2390
2391 let mut request = client
2392 .delete(&url)
2393 .header("mcp-session-id", &session_id)
2394 .timeout(SESSION_DELETE_TIMEOUT);
2395
2396 for (key, value) in &headers {
2397 request = request.header(key.as_str(), value.as_str());
2398 }
2399
2400 #[cfg(feature = "oauth-client")]
2402 if let Some(ref provider) = token_provider
2403 && let Ok(token) = provider.get_token().await
2404 && let Ok(auth_headers) = bearer_headers(&token)
2405 {
2406 request = request.headers(auth_headers);
2407 }
2408
2409 let _ = request.send().await;
2410}
2411
2412struct SseLoopParams {
2418 url: String,
2419 client: reqwest::Client,
2420 session_id: String,
2421 protocol_version: Option<String>,
2422 tx: mpsc::Sender<String>,
2423 last_event_id: Arc<RwLock<Option<String>>>,
2424 sse_retry_delay: Arc<RwLock<Option<Duration>>>,
2425 reconnect_signal: Arc<Notify>,
2426 connected: Arc<AtomicBool>,
2427 config: HttpClientConfig,
2428 #[cfg(feature = "oauth-client")]
2429 token_provider: Option<Arc<dyn TokenProvider>>,
2430}
2431
2432async fn sse_stream_loop(params: SseLoopParams) {
2438 let SseLoopParams {
2439 url,
2440 client,
2441 session_id,
2442 protocol_version,
2443 tx,
2444 last_event_id,
2445 sse_retry_delay,
2446 reconnect_signal,
2447 connected,
2448 config,
2449 #[cfg(feature = "oauth-client")]
2450 token_provider,
2451 } = params;
2452 let mut reconnect_attempts = 0u32;
2453
2454 loop {
2455 if !connected.load(Ordering::Acquire) {
2456 break;
2457 }
2458
2459 let mut request = client
2460 .get(&url)
2461 .header("Accept", "text/event-stream")
2462 .header("mcp-session-id", &session_id);
2463
2464 if let Some(ref version) = protocol_version {
2465 request = request.header("mcp-protocol-version", version);
2466 }
2467
2468 for (key, value) in &config.headers {
2469 request = request.header(key.as_str(), value.as_str());
2470 }
2471
2472 #[cfg(feature = "oauth-client")]
2474 if let Some(ref provider) = token_provider {
2475 match provider.get_token().await {
2476 Ok(token) => match bearer_headers(&token) {
2477 Ok(headers) => request = request.headers(headers),
2478 Err(error) => {
2479 tracing::warn!(%error, "Token provider failed for SSE connection");
2480 break;
2481 }
2482 },
2483 Err(_) => {
2484 tracing::warn!("Token provider failed for SSE connection");
2485 break;
2486 }
2487 }
2488 }
2489
2490 if let Some(ref lei) = *last_event_id.read().await {
2492 request = request.header("Last-Event-ID", lei.clone());
2493 }
2494
2495 let response = match request.send().await {
2496 Ok(r) if r.status().is_success() => {
2497 reconnect_attempts = 0;
2498 r
2499 }
2500 Ok(r) => {
2501 tracing::warn!(status = %r.status(), "SSE connection rejected");
2502 break;
2503 }
2504 Err(e) => {
2505 tracing::warn!(error = %request_error_message(&e), "SSE connection failed");
2506 if !config.sse_reconnect || reconnect_attempts >= config.max_sse_reconnect_attempts
2507 {
2508 break;
2509 }
2510 reconnect_attempts += 1;
2511 let delay = sse_retry_delay
2512 .read()
2513 .await
2514 .unwrap_or(config.sse_reconnect_delay);
2515 tokio::time::sleep(delay).await;
2516 continue;
2517 }
2518 };
2519
2520 let mut stream = response.bytes_stream();
2522 let mut parser = SseParser::with_limit(config.max_sse_event_size);
2523
2524 use futures::StreamExt;
2525 loop {
2526 tokio::select! {
2527 chunk = stream.next() => {
2528 match chunk {
2529 Some(Ok(bytes)) => {
2530 let text = String::from_utf8_lossy(&bytes);
2531 let events = match parser.feed(&text) {
2532 Ok(events) => events,
2533 Err(e) => {
2534 tracing::error!(error = %e, "SSE stream terminated");
2539 connected.store(false, Ordering::Release);
2540 return;
2541 }
2542 };
2543 for event in events {
2544 if let Some(ref id) = event.id {
2545 *last_event_id.write().await = Some(id.clone());
2546 }
2547 if let Some(retry_ms) = event.retry {
2548 *sse_retry_delay.write().await = Some(Duration::from_millis(retry_ms));
2549 }
2550 if !event.data.is_empty() && tx.send(event.data).await.is_err() {
2551 return; }
2553 }
2554 }
2555 Some(Err(e)) => {
2556 tracing::warn!(error = %request_error_message(&e), "SSE stream error");
2557 break;
2558 }
2559 None => {
2560 tracing::debug!("SSE stream ended");
2561 break;
2562 }
2563 }
2564 }
2565 _ = reconnect_signal.notified() => {
2566 tracing::debug!("SSE reconnect signal received, closing current stream");
2567 break;
2568 }
2569 }
2570 }
2571
2572 if !config.sse_reconnect
2574 || !connected.load(Ordering::Acquire)
2575 || reconnect_attempts >= config.max_sse_reconnect_attempts
2576 {
2577 break;
2578 }
2579 reconnect_attempts += 1;
2580 let delay = sse_retry_delay
2581 .read()
2582 .await
2583 .unwrap_or(config.sse_reconnect_delay);
2584 tracing::info!(
2585 attempt = reconnect_attempts,
2586 max = config.max_sse_reconnect_attempts,
2587 delay_ms = delay.as_millis() as u64,
2588 "Reconnecting SSE stream"
2589 );
2590 tokio::time::sleep(delay).await;
2591 }
2592}
2593
2594fn transport_error_frame(id: &serde_json::Value, message: &str) -> String {
2612 serde_json::json!({
2613 "jsonrpc": "2.0",
2614 "id": id,
2615 "error": { "code": -32000, "message": message },
2616 })
2617 .to_string()
2618}
2619
2620fn transport_error_frame_with_data(
2622 id: &serde_json::Value,
2623 message: &str,
2624 data: serde_json::Value,
2625) -> String {
2626 serde_json::json!({
2627 "jsonrpc": "2.0",
2628 "id": id,
2629 "error": { "code": -32000, "message": message, "data": data },
2630 })
2631 .to_string()
2632}
2633
2634fn status_ends_transport(status: reqwest::StatusCode) -> bool {
2637 matches!(
2638 status,
2639 reqwest::StatusCode::NOT_FOUND | reqwest::StatusCode::GONE
2640 )
2641}
2642
2643fn retry_after_header(headers: &reqwest::header::HeaderMap) -> Option<String> {
2645 headers
2646 .get(reqwest::header::RETRY_AFTER)
2647 .and_then(|value| value.to_str().ok())
2648 .map(|value| value.trim().to_string())
2649}
2650
2651fn www_authenticate_header(headers: &reqwest::header::HeaderMap) -> Option<String> {
2654 let values: Vec<&str> = headers
2655 .get_all(reqwest::header::WWW_AUTHENTICATE)
2656 .iter()
2657 .filter_map(|value| value.to_str().ok())
2658 .map(str::trim)
2659 .collect();
2660 (!values.is_empty()).then(|| values.join(", "))
2661}
2662
2663fn status_error_data(
2666 status: reqwest::StatusCode,
2667 retry_after: &Option<String>,
2668 www_authenticate: &Option<String>,
2669) -> serde_json::Value {
2670 let mut data = serde_json::json!({ "httpStatus": status.as_u16() });
2671 if let Some(raw) = retry_after {
2672 data["retryAfter"] = serde_json::Value::String(raw.clone());
2673 }
2674 if let Some(challenge) = www_authenticate {
2675 data["wwwAuthenticate"] = serde_json::Value::String(challenge.clone());
2676 }
2677 data
2678}
2679
2680fn status_annotates_jsonrpc_error(status: reqwest::StatusCode) -> bool {
2688 matches!(status.as_u16(), 401 | 403 | 429)
2689}
2690
2691fn merge_status_into_error(frame: &mut serde_json::Value, status_data: serde_json::Value) {
2700 let Some(error) = frame
2701 .get_mut("error")
2702 .and_then(serde_json::Value::as_object_mut)
2703 else {
2704 return;
2705 };
2706 let data = error.entry("data").or_insert(serde_json::Value::Null);
2707 match data {
2708 serde_json::Value::Null => *data = status_data,
2709 serde_json::Value::Object(existing) => {
2710 if let serde_json::Value::Object(additions) = status_data {
2711 for (key, value) in additions {
2712 existing.entry(key).or_insert(value);
2713 }
2714 }
2715 }
2716 other => {
2717 let original = other.take();
2718 let mut wrapped = status_data;
2719 wrapped["serverData"] = original;
2720 *other = wrapped;
2721 }
2722 }
2723}
2724
2725fn session_expired_frame(id: &serde_json::Value) -> String {
2728 serde_json::json!({
2729 "jsonrpc": "2.0",
2730 "id": id,
2731 "error": { "code": -32005, "message": "Session not found (HTTP 404)" },
2732 })
2733 .to_string()
2734}
2735
2736#[derive(Debug, Clone, PartialEq, Eq)]
2761pub struct HttpStatusDetails {
2762 pub status: u16,
2764 pub retry_after: Option<String>,
2767 pub www_authenticate: Option<String>,
2770}
2771
2772impl HttpStatusDetails {
2773 pub fn from_error(error: &Error) -> Option<Self> {
2776 let Error::JsonRpc(error) = error else {
2777 return None;
2778 };
2779 let data = error.data.as_ref()?;
2780 let status = u16::try_from(data.get("httpStatus")?.as_u64()?).ok()?;
2781 let retry_after = data
2782 .get("retryAfter")
2783 .and_then(|value| value.as_str())
2784 .map(str::to_string);
2785 let www_authenticate = data
2786 .get("wwwAuthenticate")
2787 .and_then(|value| value.as_str())
2788 .map(str::to_string);
2789 Some(Self {
2790 status,
2791 retry_after,
2792 www_authenticate,
2793 })
2794 }
2795
2796 pub fn retry_after_delay(&self) -> Option<Duration> {
2799 self.retry_after
2800 .as_deref()?
2801 .parse::<u64>()
2802 .ok()
2803 .map(Duration::from_secs)
2804 }
2805}
2806
2807const ACCEPTED_WITHOUT_RESPONSE: &str =
2810 "server answered the request with 202 Accepted instead of a response";
2811
2812const NO_RESPONSE_IN_BODY: &str = "server response did not contain a reply to this request";
2814
2815fn frames_answer_request(frames: &[String], id: &serde_json::Value) -> bool {
2818 frames.iter().any(|frame| {
2819 serde_json::from_str::<serde_json::Value>(frame).is_ok_and(|value| {
2820 value
2821 .get("id")
2822 .is_some_and(|actual| json_request_ids_match(actual, id))
2823 && (value.get("result").is_some() || value.get("error").is_some())
2824 })
2825 })
2826}
2827
2828fn json_request_ids_match(left: &serde_json::Value, right: &serde_json::Value) -> bool {
2829 left == right
2830 || (RequestId::from_json(left)
2831 .is_some_and(|left| Some(left) == RequestId::from_json(right)))
2832 || match (left, right) {
2833 (serde_json::Value::Number(number), serde_json::Value::String(value))
2834 | (serde_json::Value::String(value), serde_json::Value::Number(number)) => number
2835 .as_i64()
2836 .is_some_and(|number| value.parse::<i64>() == Ok(number)),
2837 _ => false,
2838 }
2839}
2840
2841fn extract_json_messages(body: &str) -> Vec<String> {
2842 let trimmed = crate::framing::clean_input_line(body);
2845 if trimmed.is_empty() {
2846 return Vec::new();
2847 }
2848
2849 let looks_like_sse = trimmed.starts_with("event:")
2851 || trimmed.starts_with("data:")
2852 || trimmed.starts_with("id:")
2853 || trimmed.starts_with(':');
2854
2855 if looks_like_sse {
2856 let mut parser = SseParser::new();
2859 let events = parser.feed(body).unwrap_or_default();
2860 events.into_iter().map(|e| e.data).collect()
2861 } else {
2862 vec![trimmed.to_string()]
2863 }
2864}
2865
2866#[derive(Debug)]
2868struct SseEvent {
2869 id: Option<String>,
2871 data: String,
2873 retry: Option<u64>,
2875}
2876
2877struct SseParser {
2886 buffer: String,
2888 current_id: Option<String>,
2890 current_data: Vec<String>,
2891 current_retry: Option<u64>,
2892 data_len: usize,
2894 max_event_size: usize,
2896 at_stream_start: bool,
2902}
2903
2904impl SseParser {
2905 fn new() -> Self {
2907 Self::with_limit(usize::MAX)
2908 }
2909
2910 fn with_limit(max_event_size: usize) -> Self {
2913 Self {
2914 buffer: String::new(),
2915 current_id: None,
2916 current_data: Vec::new(),
2917 current_retry: None,
2918 data_len: 0,
2919 max_event_size,
2920 at_stream_start: true,
2921 }
2922 }
2923
2924 fn feed(&mut self, text: &str) -> Result<Vec<SseEvent>> {
2930 let text = if std::mem::take(&mut self.at_stream_start) {
2933 text.strip_prefix('\u{feff}').unwrap_or(text)
2934 } else {
2935 text
2936 };
2937 self.buffer.push_str(text);
2938 let mut events = Vec::new();
2939
2940 while let Some(newline_pos) = self.buffer.find('\n') {
2942 let line = self.buffer[..newline_pos]
2943 .trim_end_matches('\r')
2944 .to_string();
2945 self.buffer = self.buffer[newline_pos + 1..].to_string();
2946
2947 if line.is_empty() {
2948 if !self.current_data.is_empty() || self.current_retry.is_some() {
2950 events.push(SseEvent {
2951 id: self.current_id.take(),
2952 data: self.current_data.join("\n"),
2953 retry: self.current_retry.take(),
2954 });
2955 self.current_data.clear();
2956 self.data_len = 0;
2957 }
2958 self.current_id = None;
2959 self.current_retry = None;
2960 } else if let Some(value) = line.strip_prefix("id:") {
2961 let trimmed = value.trim();
2962 if !trimmed.is_empty() {
2963 self.current_id = Some(trimmed.to_string());
2964 }
2965 } else if let Some(value) = line.strip_prefix("data:") {
2966 let data = value.trim().to_string();
2967 self.data_len += data.len();
2968 self.current_data.push(data);
2969 } else if let Some(value) = line.strip_prefix("retry:") {
2970 self.current_retry = value.trim().parse().ok();
2971 }
2972 }
2975
2976 let buffered = self.buffer.len() + self.data_len;
2980 if buffered > self.max_event_size {
2981 return Err(Error::SseEventTooLarge {
2982 size: buffered,
2983 limit: self.max_event_size,
2984 });
2985 }
2986
2987 Ok(events)
2988 }
2989}
2990
2991#[cfg(test)]
2992mod tests {
2993 use super::*;
2994 #[cfg(all(feature = "oauth-client", feature = "http"))]
2995 use crate::client::{
2996 OAuthAuthorizationAction, OAuthAuthorizationFlow, OAuthAuthorizationHandler,
2997 OAuthAuthorizationRequest, OAuthRedirectPolicy,
2998 };
2999
3000 #[cfg(all(feature = "oauth-client", feature = "http"))]
3008 #[derive(Clone, Default)]
3009 struct CachingProvider {
3010 minted: std::sync::Arc<std::sync::atomic::AtomicUsize>,
3011 invalidated: std::sync::Arc<std::sync::atomic::AtomicUsize>,
3012 cached: std::sync::Arc<tokio::sync::RwLock<Option<String>>>,
3013 }
3014
3015 #[cfg(all(feature = "oauth-client", feature = "http"))]
3016 #[async_trait::async_trait]
3017 impl TokenProvider for CachingProvider {
3018 async fn get_token(&self) -> std::result::Result<String, OAuthClientError> {
3019 use std::sync::atomic::Ordering;
3020 if let Some(token) = self.cached.read().await.clone() {
3021 return Ok(token);
3022 }
3023 let mut cached = self.cached.write().await;
3024 if let Some(token) = cached.clone() {
3025 return Ok(token);
3026 }
3027 let token = format!("token-{}", self.minted.fetch_add(1, Ordering::SeqCst));
3028 *cached = Some(token.clone());
3029 Ok(token)
3030 }
3031
3032 async fn invalidate(&self) {
3033 use std::sync::atomic::Ordering;
3034 self.invalidated.fetch_add(1, Ordering::SeqCst);
3035 *self.cached.write().await = None;
3036 }
3037 }
3038
3039 #[cfg(feature = "oauth-client")]
3042 #[derive(Clone)]
3043 struct StuckProvider;
3044
3045 #[cfg(feature = "oauth-client")]
3046 #[async_trait::async_trait]
3047 impl TokenProvider for StuckProvider {
3048 async fn get_token(&self) -> std::result::Result<String, OAuthClientError> {
3049 Ok("stale".to_string())
3050 }
3051 }
3052
3053 #[cfg(all(feature = "oauth-client", feature = "http"))]
3058 async fn unauthorizing_server(
3059 challenge: bool,
3060 seen: std::sync::Arc<std::sync::atomic::AtomicUsize>,
3061 ) -> (String, tokio::task::JoinHandle<()>) {
3062 use axum::http::{HeaderMap, StatusCode};
3063 use axum::response::IntoResponse;
3064 use std::sync::atomic::Ordering;
3065
3066 let app = axum::Router::new().route(
3067 "/",
3068 axum::routing::post(move |headers: HeaderMap| {
3069 let seen = seen.clone();
3070 async move {
3071 seen.fetch_add(1, Ordering::SeqCst);
3072 let accepted = headers
3073 .get("authorization")
3074 .and_then(|v| v.to_str().ok())
3075 .is_some_and(|v| v != "Bearer token-0" && v != "Bearer stale");
3076 if accepted {
3077 return (StatusCode::OK, "{}").into_response();
3078 }
3079 if challenge {
3080 (
3081 StatusCode::UNAUTHORIZED,
3082 [(
3083 "www-authenticate",
3084 r#"Bearer error="invalid_token", error_description="expired""#,
3085 )],
3086 "",
3087 )
3088 .into_response()
3089 } else {
3090 (StatusCode::UNAUTHORIZED, "").into_response()
3091 }
3092 }
3093 }),
3094 );
3095
3096 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3097 let url = format!("http://127.0.0.1:{}", listener.local_addr().unwrap().port());
3098 let handle = tokio::spawn(async move {
3099 let _ = axum::serve(listener, app).await;
3100 });
3101 (url, handle)
3102 }
3103
3104 #[cfg(all(feature = "oauth-client", feature = "http"))]
3107 async fn send_with(
3108 url: &str,
3109 provider: std::sync::Arc<dyn TokenProvider>,
3110 ) -> reqwest::StatusCode {
3111 let token = provider.get_token().await.unwrap();
3112 let request = reqwest::Client::new()
3113 .post(url)
3114 .headers(bearer_headers(&token).unwrap());
3115 match send_http_request(request, url, "tools/call", Some(provider), None, 0).await {
3116 Ok(response) => response.status(),
3117 Err(_) => panic!("the request itself must not fail"),
3118 }
3119 }
3120
3121 #[cfg(all(feature = "oauth-client", feature = "http"))]
3124 #[tokio::test]
3125 async fn a_401_challenge_replaces_the_rejected_token_and_retries() {
3126 use std::sync::atomic::Ordering;
3127 let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3128 let (url, _server) = unauthorizing_server(true, seen.clone()).await;
3129 let provider = CachingProvider::default();
3130
3131 let status = send_with(&url, std::sync::Arc::new(provider.clone())).await;
3132
3133 assert_eq!(status, reqwest::StatusCode::OK, "the retry must succeed");
3134 assert_eq!(
3135 provider.invalidated.load(Ordering::SeqCst),
3136 1,
3137 "without discarding it, the retry re-sends the rejected token"
3138 );
3139 assert_eq!(provider.minted.load(Ordering::SeqCst), 2);
3140 assert_eq!(seen.load(Ordering::SeqCst), 2, "exactly one retry");
3141 }
3142
3143 #[cfg(all(feature = "oauth-client", feature = "http"))]
3146 #[tokio::test]
3147 async fn a_provider_that_cannot_re_authenticate_stops_after_one_retry() {
3148 use std::sync::atomic::Ordering;
3149 let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3150 let (url, _server) = unauthorizing_server(true, seen.clone()).await;
3151
3152 let status = send_with(&url, std::sync::Arc::new(StuckProvider)).await;
3153
3154 assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
3155 assert_eq!(
3156 seen.load(Ordering::SeqCst),
3157 2,
3158 "one original and one retry, and no more"
3159 );
3160 }
3161
3162 #[cfg(all(feature = "oauth-client", feature = "http"))]
3166 #[tokio::test]
3167 async fn a_401_without_a_challenge_is_not_retried() {
3168 use std::sync::atomic::Ordering;
3169 let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3170 let (url, _server) = unauthorizing_server(false, seen.clone()).await;
3171 let provider = CachingProvider::default();
3172
3173 let status = send_with(&url, std::sync::Arc::new(provider.clone())).await;
3174
3175 assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
3176 assert_eq!(seen.load(Ordering::SeqCst), 1, "no retry");
3177 assert_eq!(provider.invalidated.load(Ordering::SeqCst), 0);
3178 }
3179
3180 #[cfg(feature = "oauth-client")]
3183 #[tokio::test]
3184 async fn invalidate_defaults_to_a_no_op() {
3185 let provider = StuckProvider;
3186 provider.invalidate().await;
3187 assert_eq!(provider.get_token().await.unwrap(), "stale");
3188 }
3189
3190 #[cfg(all(feature = "oauth-client", feature = "http"))]
3199 fn parse_form_body(body: &[u8]) -> HashMap<String, String> {
3200 String::from_utf8_lossy(body)
3201 .split('&')
3202 .filter(|pair| !pair.is_empty())
3203 .filter_map(|pair| {
3204 let mut parts = pair.splitn(2, '=');
3205 let key = urlencoding::decode(parts.next()?).ok()?.into_owned();
3206 let value = urlencoding::decode(parts.next().unwrap_or(""))
3207 .ok()?
3208 .into_owned();
3209 Some((key, value))
3210 })
3211 .collect()
3212 }
3213
3214 #[cfg(all(feature = "oauth-client", feature = "http"))]
3221 async fn spawn_resource_server_fixture()
3222 -> (String, std::sync::Arc<std::sync::atomic::AtomicUsize>) {
3223 use axum::body::Bytes;
3224 use axum::http::{HeaderMap, StatusCode};
3225 use axum::response::IntoResponse;
3226 use std::sync::atomic::Ordering;
3227
3228 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3229 let base = format!("http://127.0.0.1:{}", listener.local_addr().unwrap().port());
3230 let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3231
3232 let mcp_base = base.clone();
3233 let mcp_seen = seen.clone();
3234 let prm_base = base.clone();
3235 let as_base = base.clone();
3236
3237 let app = axum::Router::new()
3238 .route(
3239 "/mcp",
3240 axum::routing::post(move |headers: HeaderMap| {
3241 let seen = mcp_seen.clone();
3242 let base = mcp_base.clone();
3243 async move {
3244 seen.fetch_add(1, Ordering::SeqCst);
3245 let accepted = headers
3246 .get("authorization")
3247 .and_then(|v| v.to_str().ok())
3248 .is_some_and(|v| v == "Bearer refreshed-access-token");
3249 if accepted {
3250 return (StatusCode::OK, "{}").into_response();
3251 }
3252 (
3253 StatusCode::UNAUTHORIZED,
3254 [(
3255 "www-authenticate",
3256 format!(
3257 r#"Bearer error="invalid_token", resource_metadata="{base}/.well-known/oauth-protected-resource""#
3258 ),
3259 )],
3260 "",
3261 )
3262 .into_response()
3263 }
3264 }),
3265 )
3266 .route(
3267 "/.well-known/oauth-protected-resource",
3268 axum::routing::get(move || {
3269 let base = prm_base.clone();
3270 async move {
3271 axum::Json(serde_json::json!({
3272 "resource": format!("{base}/mcp"),
3273 "authorization_servers": [format!("{base}/as")],
3274 "scopes_supported": ["mcp.scope"]
3275 }))
3276 }
3277 }),
3278 )
3279 .route(
3280 "/.well-known/oauth-authorization-server/as",
3281 axum::routing::get(move || {
3282 let base = as_base.clone();
3283 async move {
3284 axum::Json(serde_json::json!({
3285 "issuer": format!("{base}/as"),
3286 "authorization_endpoint": format!("{base}/as/authorize"),
3287 "token_endpoint": format!("{base}/as/token"),
3288 "code_challenge_methods_supported": ["S256"],
3289 "token_endpoint_auth_methods_supported": ["none"]
3290 }))
3291 }
3292 }),
3293 )
3294 .route(
3295 "/as/token",
3296 axum::routing::post(move |body: Bytes| async move {
3297 let fields = parse_form_body(&body);
3298 match fields.get("grant_type").map(String::as_str) {
3299 Some("authorization_code") => axum::Json(serde_json::json!({
3300 "access_token": "initial-access-token",
3301 "token_type": "Bearer",
3302 "expires_in": 3600,
3303 "refresh_token": "refresh-token-1",
3304 "scope": "mcp.scope"
3305 }))
3306 .into_response(),
3307 Some("refresh_token") => axum::Json(serde_json::json!({
3308 "access_token": "refreshed-access-token",
3309 "token_type": "Bearer",
3310 "expires_in": 3600,
3311 "scope": "mcp.scope"
3312 }))
3313 .into_response(),
3314 other => (
3315 StatusCode::BAD_REQUEST,
3316 format!("unexpected grant_type: {other:?}"),
3317 )
3318 .into_response(),
3319 }
3320 }),
3321 );
3322
3323 tokio::spawn(async move {
3324 let _ = axum::serve(listener, app).await;
3325 });
3326 (base, seen)
3327 }
3328
3329 #[cfg(all(feature = "oauth-client", feature = "http"))]
3333 #[derive(Clone, Default)]
3334 struct ImmediateCallbackHandler;
3335
3336 #[cfg(all(feature = "oauth-client", feature = "http"))]
3337 #[async_trait::async_trait]
3338 impl OAuthAuthorizationHandler for ImmediateCallbackHandler {
3339 async fn authorize(
3340 &self,
3341 request: OAuthAuthorizationRequest,
3342 ) -> std::result::Result<OAuthAuthorizationAction, OAuthClientError> {
3343 let authorization_url = reqwest::Url::parse(&request.authorization_url).unwrap();
3344 let state = authorization_url
3345 .query_pairs()
3346 .find(|(name, _)| name == "state")
3347 .unwrap()
3348 .1
3349 .into_owned();
3350 let mut callback = reqwest::Url::parse(&request.redirect_uri).unwrap();
3351 callback
3352 .query_pairs_mut()
3353 .append_pair("code", "authorization-code")
3354 .append_pair("state", &state)
3355 .append_pair("iss", &request.issuer);
3356 Ok(OAuthAuthorizationAction::CallbackUrl(callback.to_string()))
3357 }
3358 }
3359
3360 #[cfg(all(feature = "oauth-client", feature = "http"))]
3380 #[tokio::test]
3381 async fn a_real_auth_code_flow_provider_reauthorizes_through_invalidate() {
3382 use std::sync::atomic::Ordering;
3383
3384 let (base, seen) = spawn_resource_server_fixture().await;
3385 let resource = format!("{base}/mcp");
3386
3387 let flow = OAuthAuthorizationFlow::builder(&resource)
3388 .redirect_policy(OAuthRedirectPolicy::fixed(format!("{base}/callback")))
3389 .pre_registered_client("test-client", None)
3390 .authorization_handler(ImmediateCallbackHandler)
3391 .build()
3392 .unwrap();
3393
3394 flow.authorize(["mcp.scope"]).await.unwrap();
3395 assert_eq!(
3396 flow.get_token().await.unwrap(),
3397 "initial-access-token",
3398 "sanity: the code exchange installed the token the fixture rejects"
3399 );
3400
3401 let status = send_with(&resource, std::sync::Arc::new(flow.clone())).await;
3402
3403 assert_eq!(status, reqwest::StatusCode::OK, "the retry must succeed");
3404 assert_eq!(
3405 flow.get_token().await.unwrap(),
3406 "refreshed-access-token",
3407 "invalidate() must have caused get_token() to refresh"
3408 );
3409 assert_eq!(
3410 seen.load(Ordering::SeqCst),
3411 3,
3412 "discovery probe, rejected first attempt, successful retry"
3413 );
3414 }
3415
3416 #[test]
3424 fn drop_outside_a_runtime_does_not_panic() {
3425 let mut transport = HttpClientTransport::new("http://127.0.0.1:1");
3426 transport.session_id = Some("session-outside-runtime".to_string());
3427 transport.protocol_version = Some(crate::protocol::LATEST_PROTOCOL_VERSION.to_string());
3428 drop(transport);
3429 }
3430
3431 #[test]
3436 fn test_parse_complete_event() {
3437 let mut parser = SseParser::new();
3438 let events = parser
3439 .feed("id: 1\nevent: message\ndata: {\"hello\":\"world\"}\n\n")
3440 .unwrap();
3441
3442 assert_eq!(events.len(), 1);
3443 assert_eq!(events[0].id, Some("1".to_string()));
3444 assert_eq!(events[0].data, "{\"hello\":\"world\"}");
3445 }
3446
3447 #[test]
3450 fn test_parse_strips_a_bom_at_the_start_of_the_stream() {
3451 let mut parser = SseParser::new();
3452 let events = parser
3453 .feed("\u{feff}data: {\"hello\":\"world\"}\n\n")
3454 .unwrap();
3455
3456 assert_eq!(events.len(), 1, "the opening event must survive a BOM");
3457 assert_eq!(events[0].data, "{\"hello\":\"world\"}");
3458 }
3459
3460 #[test]
3464 fn test_parse_keeps_a_bom_inside_the_stream() {
3465 let mut parser = SseParser::new();
3466 assert!(parser.feed("data: first\n\n").unwrap().len() == 1);
3467
3468 let events = parser.feed("data: \u{feff}second\n\n").unwrap();
3469 assert_eq!(events.len(), 1);
3470 assert_eq!(events[0].data, "\u{feff}second");
3471 }
3472
3473 #[test]
3476 fn test_extract_json_messages_strips_a_leading_bom() {
3477 let json = r#"{"jsonrpc":"2.0","id":1,"result":{}}"#;
3478 assert_eq!(
3479 extract_json_messages(&format!("\u{feff}{json}")),
3480 vec![json.to_string()],
3481 );
3482
3483 let sse = extract_json_messages(&format!("\u{feff}data: {json}\n\n"));
3484 assert_eq!(
3485 sse,
3486 vec![json.to_string()],
3487 "a BOM must not hide an SSE body"
3488 );
3489 }
3490
3491 #[test]
3492 fn test_parse_multiple_events() {
3493 let mut parser = SseParser::new();
3494 let events = parser
3495 .feed("id: 1\ndata: first\n\nid: 2\ndata: second\n\nid: 3\ndata: third\n\n")
3496 .unwrap();
3497
3498 assert_eq!(events.len(), 3);
3499 assert_eq!(events[0].data, "first");
3500 assert_eq!(events[1].data, "second");
3501 assert_eq!(events[2].data, "third");
3502 assert_eq!(events[0].id, Some("1".to_string()));
3503 assert_eq!(events[1].id, Some("2".to_string()));
3504 assert_eq!(events[2].id, Some("3".to_string()));
3505 }
3506
3507 #[test]
3508 fn test_parse_partial_chunks() {
3509 let mut parser = SseParser::new();
3510
3511 let events = parser.feed("id: 1\nda").unwrap();
3513 assert!(events.is_empty());
3514
3515 let events = parser.feed("ta: hello\n\n").unwrap();
3517 assert_eq!(events.len(), 1);
3518 assert_eq!(events[0].id, Some("1".to_string()));
3519 assert_eq!(events[0].data, "hello");
3520 }
3521
3522 #[test]
3523 fn test_parse_multiline_data() {
3524 let mut parser = SseParser::new();
3525 let events = parser
3526 .feed("id: 1\ndata: line1\ndata: line2\ndata: line3\n\n")
3527 .unwrap();
3528
3529 assert_eq!(events.len(), 1);
3530 assert_eq!(events[0].data, "line1\nline2\nline3");
3531 }
3532
3533 #[test]
3534 fn test_parse_comment_lines() {
3535 let mut parser = SseParser::new();
3536 let events = parser.feed(": keep-alive\nid: 1\ndata: hello\n\n").unwrap();
3537
3538 assert_eq!(events.len(), 1);
3539 assert_eq!(events[0].data, "hello");
3540 }
3541
3542 #[test]
3543 fn test_parse_event_without_id() {
3544 let mut parser = SseParser::new();
3545 let events = parser.feed("data: no-id-event\n\n").unwrap();
3546
3547 assert_eq!(events.len(), 1);
3548 assert_eq!(events[0].id, None);
3549 assert_eq!(events[0].data, "no-id-event");
3550 }
3551
3552 #[test]
3553 fn test_empty_data_no_event() {
3554 let mut parser = SseParser::new();
3555 let events = parser.feed("id: 1\n\n").unwrap();
3556
3557 assert!(events.is_empty());
3559 }
3560
3561 #[test]
3562 fn test_parse_crlf_line_endings() {
3563 let mut parser = SseParser::new();
3564 let events = parser.feed("id: 1\r\ndata: crlf\r\n\r\n").unwrap();
3565
3566 assert_eq!(events.len(), 1);
3567 assert_eq!(events[0].data, "crlf");
3568 }
3569
3570 #[test]
3571 fn test_parse_json_data() {
3572 let mut parser = SseParser::new();
3573 let json = r#"{"jsonrpc":"2.0","method":"notifications/progress","params":{"token":"t1","progress":50}}"#;
3574 let input = format!("id: 42\nevent: message\ndata: {}\n\n", json);
3575 let events = parser.feed(&input).unwrap();
3576
3577 assert_eq!(events.len(), 1);
3578 assert_eq!(events[0].id, Some("42".to_string()));
3579
3580 let parsed: serde_json::Value = serde_json::from_str(&events[0].data).unwrap();
3582 assert_eq!(parsed["method"], "notifications/progress");
3583 }
3584
3585 #[test]
3586 fn test_event_exceeding_limit_is_rejected() {
3587 let mut parser = SseParser::with_limit(64);
3588
3589 let big = "data: ".to_string() + &"x".repeat(128);
3591 let err = parser.feed(&big).unwrap_err();
3592 match err {
3593 Error::SseEventTooLarge { size, limit } => {
3594 assert!(size > 64, "size {} should exceed limit", size);
3595 assert_eq!(limit, 64);
3596 }
3597 other => panic!("expected SseEventTooLarge, got {:?}", other),
3598 }
3599 }
3600
3601 #[test]
3602 fn test_accumulated_data_lines_count_toward_limit() {
3603 let mut parser = SseParser::with_limit(64);
3604
3605 let mut result = Ok(Vec::new());
3607 for _ in 0..10 {
3608 result = parser.feed("data: 0123456789\n");
3609 if result.is_err() {
3610 break;
3611 }
3612 }
3613 assert!(matches!(result, Err(Error::SseEventTooLarge { .. })));
3614 }
3615
3616 #[test]
3617 fn test_events_within_limit_pass() {
3618 let mut parser = SseParser::with_limit(64);
3619 let events = parser.feed("data: hello\n\ndata: world\n\n").unwrap();
3620 assert_eq!(events.len(), 2);
3621 }
3622
3623 #[test]
3628 fn test_default_config() {
3629 let config = HttpClientConfig::default();
3630 assert!(config.auto_sse);
3631 assert_eq!(config.channel_capacity, 256);
3632 assert_eq!(config.request_timeout, Duration::from_secs(30));
3633 assert!(config.sse_reconnect);
3634 assert_eq!(config.sse_reconnect_delay, Duration::from_secs(1));
3635 assert_eq!(config.max_sse_reconnect_attempts, 5);
3636 assert!(config.headers.is_empty());
3637 }
3638
3639 #[test]
3644 fn test_new_transport() {
3645 let transport = HttpClientTransport::new("http://localhost:3000");
3646 assert_eq!(transport.url, "http://localhost:3000");
3647 assert!(transport.session_id.is_none());
3648 assert!(transport.protocol_version.is_none());
3649 assert!(transport.is_connected());
3650 }
3651
3652 #[test]
3653 fn test_with_config() {
3654 let config = HttpClientConfig {
3655 request_timeout: Duration::from_secs(60),
3656 sse_reconnect: false,
3657 ..Default::default()
3658 };
3659 let transport = HttpClientTransport::with_config("http://example.com", config);
3660 assert_eq!(transport.url, "http://example.com");
3661 assert_eq!(transport.config.request_timeout, Duration::from_secs(60));
3662 assert!(!transport.config.sse_reconnect);
3663 }
3664
3665 #[test]
3666 fn test_with_client() {
3667 let client = reqwest::Client::new();
3668 let transport = HttpClientTransport::with_client("http://example.com", client);
3669 assert_eq!(transport.url, "http://example.com");
3670 assert!(transport.is_connected());
3671 }
3672
3673 #[test]
3678 fn test_bearer_token() {
3679 let transport =
3680 HttpClientTransport::new("http://localhost:3000").bearer_token("sk-test-token");
3681 assert_eq!(
3682 transport.config.headers.get("Authorization").unwrap(),
3683 "Bearer sk-test-token"
3684 );
3685 }
3686
3687 #[test]
3688 fn test_api_key() {
3689 let transport = HttpClientTransport::new("http://localhost:3000").api_key("sk-api-key-123");
3690 assert_eq!(
3691 transport.config.headers.get("Authorization").unwrap(),
3692 "Bearer sk-api-key-123"
3693 );
3694 }
3695
3696 #[test]
3697 fn test_api_key_header() {
3698 let transport =
3699 HttpClientTransport::new("http://localhost:3000").api_key_header("X-API-Key", "my-key");
3700 assert_eq!(transport.config.headers.get("X-API-Key").unwrap(), "my-key");
3701 assert!(!transport.config.headers.contains_key("Authorization"));
3702 }
3703
3704 #[test]
3705 fn test_basic_auth() {
3706 let transport =
3707 HttpClientTransport::new("http://localhost:3000").basic_auth("admin", "secret");
3708 let header = transport.config.headers.get("Authorization").unwrap();
3709 assert!(header.starts_with("Basic "));
3710 use base64::Engine;
3711 let decoded = base64::engine::general_purpose::STANDARD
3712 .decode(header.strip_prefix("Basic ").unwrap())
3713 .unwrap();
3714 assert_eq!(String::from_utf8(decoded).unwrap(), "admin:secret");
3715 }
3716
3717 #[test]
3718 fn test_custom_header() {
3719 let transport = HttpClientTransport::new("http://localhost:3000")
3720 .header("X-Custom", "value1")
3721 .header("X-Another", "value2");
3722 assert_eq!(transport.config.headers.get("X-Custom").unwrap(), "value1");
3723 assert_eq!(transport.config.headers.get("X-Another").unwrap(), "value2");
3724 }
3725
3726 #[test]
3727 fn test_chaining_with_config() {
3728 let config = HttpClientConfig {
3729 request_timeout: Duration::from_secs(60),
3730 ..Default::default()
3731 };
3732 let transport =
3733 HttpClientTransport::with_config("http://localhost:3000", config).bearer_token("tk");
3734 assert_eq!(transport.config.request_timeout, Duration::from_secs(60));
3735 assert_eq!(
3736 transport.config.headers.get("Authorization").unwrap(),
3737 "Bearer tk"
3738 );
3739 }
3740
3741 #[test]
3742 fn test_last_auth_wins() {
3743 let transport = HttpClientTransport::new("http://localhost:3000")
3744 .bearer_token("token1")
3745 .basic_auth("user", "pass");
3746 let header = transport.config.headers.get("Authorization").unwrap();
3747 assert!(header.starts_with("Basic "));
3748 }
3749
3750 #[test]
3751 fn test_config_bearer_token() {
3752 let config = HttpClientConfig::default().bearer_token("tk-123");
3753 assert_eq!(
3754 config.headers.get("Authorization").unwrap(),
3755 "Bearer tk-123"
3756 );
3757 }
3758
3759 #[test]
3760 fn test_config_header() {
3761 let config = HttpClientConfig::default().header("X-Foo", "bar");
3762 assert_eq!(config.headers.get("X-Foo").unwrap(), "bar");
3763 }
3764
3765 #[test]
3766 fn test_config_api_key_header() {
3767 let config = HttpClientConfig::default().api_key_header("X-Key", "secret");
3768 assert_eq!(config.headers.get("X-Key").unwrap(), "secret");
3769 }
3770
3771 #[test]
3772 fn test_config_basic_auth() {
3773 let config = HttpClientConfig::default().basic_auth("user", "pw");
3774 let header = config.headers.get("Authorization").unwrap();
3775 assert!(header.starts_with("Basic "));
3776 }
3777
3778 #[test]
3779 fn sep_2243_encodes_only_unsafe_values() {
3780 assert_eq!(encode_header_value("us west 1"), "us west 1");
3781 assert_eq!(encode_header_value(""), "");
3782 assert_eq!(encode_header_value(" padded "), "=?base64?IHBhZGRlZCA=?=");
3783 assert_eq!(
3784 encode_header_value("Hello, 世界"),
3785 "=?base64?SGVsbG8sIOS4lueVjA==?="
3786 );
3787 }
3788
3789 #[test]
3790 fn oauth_error_body_is_not_misclassified_as_jsonrpc() {
3791 assert!(!is_jsonrpc_error_response(&serde_json::json!({
3792 "error": "insufficient_scope",
3793 "error_description": "Token has insufficient scope"
3794 })));
3795 assert!(is_jsonrpc_error_response(&serde_json::json!({
3796 "jsonrpc": "2.0",
3797 "id": 1,
3798 "error": {
3799 "code": -32022,
3800 "message": "Unsupported protocol version"
3801 }
3802 })));
3803 }
3804
3805 #[test]
3806 fn sep_2243_validates_custom_header_annotations() {
3807 let mappings = custom_header_mappings(&serde_json::json!({
3808 "type": "object",
3809 "properties": {
3810 "region": {"type": "string", "x-mcp-header": "Region"},
3811 "priority": {"type": "integer", "x-mcp-header": "Priority"},
3812 "enabled": {"type": ["boolean", "null"], "x-mcp-header": "Enabled"}
3813 }
3814 }))
3815 .unwrap();
3816 assert_eq!(mappings.len(), 3);
3817
3818 for invalid in [
3819 serde_json::json!({
3822 "type": "object",
3823 "properties": {"ratio": {"type": "number", "x-mcp-header": "Ratio"}}
3824 }),
3825 serde_json::json!({
3826 "type": "object",
3827 "properties": {"ratio": {"type": ["number", "null"], "x-mcp-header": "Ratio"}}
3828 }),
3829 serde_json::json!({
3830 "type": "object",
3831 "properties": {"value": {"type": "object", "x-mcp-header": "Value"}}
3832 }),
3833 serde_json::json!({
3834 "type": "object",
3835 "properties": {
3836 "a": {"type": "string", "x-mcp-header": "Region"},
3837 "b": {"type": "string", "x-mcp-header": "region"}
3838 }
3839 }),
3840 serde_json::json!({
3841 "type": "object",
3842 "properties": {"value": {"type": "string", "x-mcp-header": "Bad Header"}}
3843 }),
3844 ] {
3845 assert!(custom_header_mappings(&invalid).is_err());
3846 }
3847 }
3848
3849 #[test]
3850 fn sep_2243_filters_invalid_tools_and_caches_valid_mappings() {
3851 let mut transport = HttpClientTransport::new("http://localhost:3000");
3852 transport.protocol_version = Some(crate::protocol::PROTOCOL_VERSION_2026_07_28.to_string());
3853 let normalized = transport.normalize_incoming_message(
3854 serde_json::json!({
3855 "jsonrpc": "2.0",
3856 "id": 1,
3857 "result": {
3858 "tools": [
3859 {
3860 "name": "valid",
3861 "inputSchema": {
3862 "type": "object",
3863 "properties": {
3864 "region": {"type": "string", "x-mcp-header": "Region"}
3865 }
3866 }
3867 },
3868 {
3869 "name": "invalid",
3870 "inputSchema": {
3871 "type": "object",
3872 "properties": {
3873 "value": {"type": "array", "x-mcp-header": "Value"}
3874 }
3875 }
3876 },
3877 {
3878 "name": "number_typed",
3879 "inputSchema": {
3880 "type": "object",
3881 "properties": {
3882 "ratio": {"type": "number", "x-mcp-header": "Ratio"}
3883 }
3884 }
3885 }
3886 ]
3887 }
3888 })
3889 .to_string(),
3890 );
3891 let parsed: serde_json::Value = serde_json::from_str(&normalized).unwrap();
3892 assert_eq!(parsed["result"]["tools"].as_array().unwrap().len(), 1);
3893 assert!(transport.tool_header_mappings.contains_key("valid"));
3894 assert!(!transport.tool_header_mappings.contains_key("invalid"));
3895 assert!(!transport.tool_header_mappings.contains_key("number_typed"));
3896 }
3897
3898 fn tools_page(id: i64, tool: &str) -> String {
3899 serde_json::json!({
3900 "jsonrpc": "2.0",
3901 "id": id,
3902 "result": {
3903 "tools": [{
3904 "name": tool,
3905 "inputSchema": {
3906 "type": "object",
3907 "properties": {
3908 "region": {"type": "string", "x-mcp-header": "Region"}
3909 }
3910 }
3911 }]
3912 }
3913 })
3914 .to_string()
3915 }
3916
3917 #[test]
3921 fn tools_list_pages_merge_header_mappings_until_list_changed() {
3922 let mut transport = HttpClientTransport::new("http://localhost:3000");
3923 transport.protocol_version = Some(crate::protocol::PROTOCOL_VERSION_2026_07_28.to_string());
3924
3925 transport
3926 .pending_tools_lists
3927 .push((serde_json::json!(1), false));
3928 transport.normalize_incoming_message(tools_page(1, "first"));
3929 transport
3930 .pending_tools_lists
3931 .push((serde_json::json!(2), true));
3932 transport.normalize_incoming_message(tools_page(2, "second"));
3933 assert!(transport.pending_tools_lists.is_empty());
3934 assert!(transport.tool_header_mappings.contains_key("first"));
3935 assert!(transport.tool_header_mappings.contains_key("second"));
3936
3937 transport
3939 .pending_tools_lists
3940 .push((serde_json::json!(3), false));
3941 transport.normalize_incoming_message(tools_page(3, "third"));
3942 assert_eq!(transport.tool_header_mappings.len(), 1);
3943 assert!(transport.tool_header_mappings.contains_key("third"));
3944
3945 transport
3946 .pending_tools_lists
3947 .push((serde_json::json!(4), true));
3948 transport.normalize_incoming_message(tools_page(4, "fourth"));
3949 assert_eq!(transport.tool_header_mappings.len(), 2);
3950
3951 let notification = serde_json::json!({
3952 "jsonrpc": "2.0",
3953 "method": "notifications/tools/list_changed"
3954 })
3955 .to_string();
3956 assert_eq!(
3957 transport.normalize_incoming_message(notification.clone()),
3958 notification
3959 );
3960 assert!(transport.tool_header_mappings.is_empty());
3961 }
3962
3963 #[test]
3965 fn tools_list_error_response_clears_pending_entry() {
3966 let mut transport = HttpClientTransport::new("http://localhost:3000");
3967 transport.protocol_version = Some(crate::protocol::PROTOCOL_VERSION_2026_07_28.to_string());
3968 transport
3969 .pending_tools_lists
3970 .push((serde_json::json!(7), true));
3971 transport.normalize_incoming_message(
3972 serde_json::json!({
3973 "jsonrpc": "2.0",
3974 "id": 7,
3975 "error": {"code": -32603, "message": "boom"}
3976 })
3977 .to_string(),
3978 );
3979 assert!(transport.pending_tools_lists.is_empty());
3980 }
3981
3982 #[tokio::test]
3992 async fn responses_are_not_tracked_in_request_tasks() {
3993 let mut transport = HttpClientTransport::new("http://127.0.0.1:1");
3994 transport.session_id = Some("test-session".to_string());
3995
3996 transport
3999 .send(r#"{"jsonrpc":"2.0","id":7,"result":{}}"#)
4000 .await
4001 .unwrap();
4002 assert!(
4003 transport.request_tasks.is_empty(),
4004 "a response must never be tracked in request_tasks"
4005 );
4006
4007 transport
4009 .send(r#"{"jsonrpc":"2.0","id":7,"method":"ping","params":{}}"#)
4010 .await
4011 .unwrap();
4012 assert!(
4013 transport.request_tasks.contains_key(&RequestId::Number(7)),
4014 "a request must still be tracked in request_tasks"
4015 );
4016
4017 transport
4020 .cancel_request(&RequestId::Number(7))
4021 .await
4022 .unwrap();
4023 assert!(transport.request_tasks.is_empty());
4024 }
4025
4026 fn url(s: &str) -> reqwest::Url {
4031 reqwest::Url::parse(s).unwrap()
4032 }
4033
4034 #[test]
4035 fn only_body_preserving_redirect_statuses_are_followed() {
4036 use reqwest::StatusCode;
4037 assert!(redirect_preserves_request(StatusCode::TEMPORARY_REDIRECT));
4038 assert!(redirect_preserves_request(StatusCode::PERMANENT_REDIRECT));
4039 for status in [
4040 StatusCode::MOVED_PERMANENTLY,
4041 StatusCode::FOUND,
4042 StatusCode::SEE_OTHER,
4043 ] {
4044 assert!(!redirect_preserves_request(status), "{status}");
4045 }
4046 }
4047
4048 #[test]
4049 fn same_origin_redirect_is_followed_by_default() {
4050 assert!(should_follow_redirect(
4051 &url("https://example.com/mcp"),
4052 &url("https://example.com/other"),
4053 false,
4054 ));
4055 }
4056
4057 #[test]
4058 fn different_port_is_not_same_origin_by_default() {
4059 assert!(!should_follow_redirect(
4060 &url("https://example.com:8443/mcp"),
4061 &url("https://example.com:9443/mcp"),
4062 false,
4063 ));
4064 }
4065
4066 #[test]
4067 fn default_port_matches_an_explicit_default_port() {
4068 assert!(should_follow_redirect(
4071 &url("https://example.com/mcp"),
4072 &url("https://example.com:443/other"),
4073 false,
4074 ));
4075 }
4076
4077 #[test]
4078 fn cross_host_redirect_is_not_followed_by_default() {
4079 assert!(!should_follow_redirect(
4080 &url("https://example.com/mcp"),
4081 &url("https://attacker.example/mcp"),
4082 false,
4083 ));
4084 }
4085
4086 #[test]
4087 fn cross_host_redirect_is_followed_when_opted_in() {
4088 assert!(should_follow_redirect(
4089 &url("https://example.com/mcp"),
4090 &url("https://attacker.example/mcp"),
4091 true,
4092 ));
4093 }
4094
4095 #[test]
4096 fn https_to_http_downgrade_is_never_followed_even_when_opted_in() {
4097 assert!(!should_follow_redirect(
4098 &url("https://example.com/mcp"),
4099 &url("http://example.com/mcp"),
4100 false,
4101 ));
4102 assert!(!should_follow_redirect(
4103 &url("https://example.com/mcp"),
4104 &url("http://attacker.example/mcp"),
4105 true,
4106 ));
4107 }
4108
4109 #[test]
4110 fn http_to_https_redirect_is_not_a_downgrade() {
4111 assert!(!should_follow_redirect(
4114 &url("http://example.com/mcp"),
4115 &url("https://example.com/mcp"),
4116 false,
4117 ));
4118 assert!(should_follow_redirect(
4119 &url("http://example.com/mcp"),
4120 &url("https://example.com/mcp"),
4121 true,
4122 ));
4123 }
4124}