mcp_streamable_proxy/
detector.rs1use reqwest::header::{ACCEPT, CONTENT_TYPE, HeaderMap, HeaderValue};
2use std::collections::HashMap;
3use std::time::Duration;
4
5pub async fn is_streamable_http(url: &str) -> bool {
20 is_streamable_http_with_headers(url, None).await
21}
22
23pub async fn is_streamable_http_with_headers(
46 url: &str,
47 custom_headers: Option<&HashMap<String, String>>,
48) -> bool {
49 let client = match reqwest::Client::builder()
51 .timeout(Duration::from_secs(5))
52 .build()
53 {
54 Ok(c) => c,
55 Err(_) => return false,
56 };
57
58 let mut headers = HeaderMap::new();
60 headers.insert(
61 ACCEPT,
62 HeaderValue::from_static("application/json, text/event-stream"),
63 );
64 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
65
66 if let Some(custom) = custom_headers {
68 for (key, value) in custom {
69 if let (Ok(name), Ok(val)) = (
70 reqwest::header::HeaderName::try_from(key.as_str()),
71 HeaderValue::from_str(value),
72 ) {
73 headers.insert(name, val);
74 }
75 }
76 }
77
78 use rmcp::model::{
80 ClientCapabilities, ClientRequest, Implementation, InitializeRequestParams,
81 ProtocolVersion, Request, RequestId,
82 };
83
84 let init_request = ClientRequest::InitializeRequest(Request::new(
85 InitializeRequestParams::new(
86 ClientCapabilities::default(),
87 Implementation::new("mcp-proxy-detector", "0.1.0"),
88 )
89 .with_protocol_version(ProtocolVersion::V_2024_11_05),
90 ));
91
92 let body = rmcp::model::ClientJsonRpcMessage::request(init_request, RequestId::Number(1));
94
95 let response = match client.post(url).headers(headers).json(&body).send().await {
97 Ok(r) => r,
98 Err(_) => return false,
99 };
100
101 let status = response.status();
102 let resp_headers = response.headers().clone();
103
104 if resp_headers.contains_key("mcp-session-id") {
106 return true;
107 }
108 if let Some(content_type) = resp_headers.get(CONTENT_TYPE)
110 && let Ok(ct) = content_type.to_str()
111 && ct.contains("text/event-stream")
112 && status.is_success()
113 {
114 return true;
115 }
116 if let Ok(json) = response.json::<serde_json::Value>().await {
118 let is_jsonrpc = json
120 .get("jsonrpc")
121 .and_then(|v| v.as_str())
122 .map(|v| v == "2.0")
123 .unwrap_or(false);
124 if is_jsonrpc {
125 return true;
126 }
127 }
128
129 if status == reqwest::StatusCode::NOT_ACCEPTABLE {
131 return true;
132 }
133
134 false
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[tokio::test]
142 async fn test_is_streamable_http_with_headers_backward_compatible() {
143 let result = is_streamable_http_with_headers("http://localhost:99999/mcp", None).await;
145 assert!(!result);
146 }
147
148 #[tokio::test]
149 async fn test_is_streamable_http_with_headers_no_panic() {
150 let mut headers = HashMap::new();
152 headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
153 let result =
154 is_streamable_http_with_headers("http://localhost:99999/mcp", Some(&headers)).await;
155 assert!(!result);
156 }
157}