use futures::StreamExt;
use reqwest::header::{ACCEPT, CONTENT_TYPE, HeaderMap, HeaderValue};
use std::collections::HashMap;
use std::sync::LazyLock;
use std::time::Duration;
use tracing::{debug, info};
static SSE_PROBE_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(10)) .build()
.unwrap_or_else(|_| reqwest::Client::new()) });
pub async fn is_sse(url: &str) -> bool {
is_sse_with_headers(url, None).await
}
fn build_candidate_urls(url: &str) -> Vec<String> {
let parsed = match url::Url::parse(url) {
Ok(u) => u,
Err(_) => return vec![url.to_string()], };
let path = parsed.path();
let path_trimmed = path.trim_end_matches('/');
if path_trimmed.ends_with("/sse") {
return vec![url.to_string()];
}
let mut sse_url = parsed.clone();
sse_url.set_path(&format!("{}/sse", path_trimmed));
vec![sse_url.to_string(), url.to_string()]
}
pub async fn is_sse_with_headers(
url: &str,
custom_headers: Option<&HashMap<String, String>>,
) -> bool {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream"));
if let Some(custom) = custom_headers {
for (key, value) in custom {
if let (Ok(name), Ok(val)) = (
reqwest::header::HeaderName::try_from(key.as_str()),
HeaderValue::from_str(value),
) {
headers.insert(name, val);
}
}
}
let has_custom_headers = custom_headers.is_some_and(|h| !h.is_empty());
let candidates = build_candidate_urls(url);
for probe_url in &candidates {
debug!("SSE probe: trying {}", probe_url);
match probe_sse_endpoint(&SSE_PROBE_CLIENT, probe_url, &headers).await {
ProbeResult::Success => {
debug!(
"SSE probe: confirmed {} is MCP SSE (endpoint event found)",
probe_url
);
return true;
}
failure => {
let reason = failure.reason();
if has_custom_headers {
info!(
"SSE probe: {} is NOT MCP SSE — {} (custom headers were used)",
probe_url, reason
);
} else {
debug!("SSE probe: {} is NOT MCP SSE — {}", probe_url, reason);
}
}
}
}
false
}
enum ProbeResult {
Success,
HttpError(u16),
WrongContentType(Option<String>),
NoEndpointEvent,
ConnectionFailed,
}
impl ProbeResult {
fn reason(&self) -> String {
match self {
ProbeResult::Success => "success".to_string(),
ProbeResult::HttpError(code) => format!("HTTP {}", code),
ProbeResult::WrongContentType(ct) => {
format!("Content-Type is {:?} (expected text/event-stream)", ct)
}
ProbeResult::NoEndpointEvent => {
"no endpoint event found in first 4KB of stream".to_string()
}
ProbeResult::ConnectionFailed => "connection failed".to_string(),
}
}
}
async fn probe_sse_endpoint(
client: &reqwest::Client,
url: &str,
headers: &HeaderMap,
) -> ProbeResult {
let response = match client.get(url).headers(headers.clone()).send().await {
Ok(r) => r,
Err(e) => {
debug!("SSE probe: connection to {} failed: {}", url, e);
return ProbeResult::ConnectionFailed;
}
};
let status = response.status();
if !status.is_success() {
let code = status.as_u16();
debug!("SSE probe: {} returned HTTP {}", url, code);
return ProbeResult::HttpError(code);
}
let content_type = response
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let is_event_stream = content_type
.as_ref()
.is_some_and(|ct| ct.contains("text/event-stream"));
if !is_event_stream {
debug!(
"SSE probe: {} Content-Type is {:?}, expected text/event-stream",
url, content_type
);
return ProbeResult::WrongContentType(content_type);
}
if read_sse_for_endpoint_event(response).await {
ProbeResult::Success
} else {
ProbeResult::NoEndpointEvent
}
}
fn contains_endpoint_event(buffer: &[u8]) -> bool {
const PATTERN_WITH_SPACE: &[u8] = b"event: endpoint";
const PATTERN_NO_SPACE: &[u8] = b"event:endpoint";
buffer
.windows(PATTERN_WITH_SPACE.len())
.any(|w| w == PATTERN_WITH_SPACE)
|| buffer
.windows(PATTERN_NO_SPACE.len())
.any(|w| w == PATTERN_NO_SPACE)
}
async fn read_sse_for_endpoint_event(response: reqwest::Response) -> bool {
let mut stream = response.bytes_stream();
let mut buffer: Vec<u8> = Vec::new();
const MAX_BYTES: usize = 4096;
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => {
buffer.extend_from_slice(&bytes);
if contains_endpoint_event(&buffer) {
return true;
}
if buffer.len() > MAX_BYTES {
return false;
}
}
Err(_) => return false,
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contains_endpoint_event_with_space() {
let data = b"event: endpoint\ndata: /messages?session_id=abc123\n\n";
assert!(contains_endpoint_event(data));
}
#[test]
fn test_contains_endpoint_event_without_space() {
let data = b"event:endpoint\ndata: /messages?session_id=abc123\n\n";
assert!(contains_endpoint_event(data));
}
#[test]
fn test_contains_endpoint_event_absent() {
let data = b"data: some data\n\n";
assert!(!contains_endpoint_event(data));
}
#[tokio::test]
async fn test_is_sse_nonexistent_server() {
let result = is_sse("http://localhost:99999/mcp").await;
assert!(!result);
}
#[tokio::test]
async fn test_is_sse_with_headers_no_panic() {
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
let result = is_sse_with_headers("http://localhost:99999/mcp", Some(&headers)).await;
assert!(!result);
}
#[tokio::test]
async fn test_candidate_urls_with_sse_suffix() {
let result = is_sse("http://localhost:99999/sse").await;
assert!(!result);
}
#[tokio::test]
async fn test_candidate_urls_with_trailing_slash() {
let result = is_sse("http://localhost:99999/mcp/").await;
assert!(!result);
}
#[test]
fn test_build_candidate_urls_with_query_params() {
let candidates = build_candidate_urls("http://host/mcp?token=abc123");
assert_eq!(candidates.len(), 2);
assert_eq!(candidates[0], "http://host/mcp/sse?token=abc123");
assert_eq!(candidates[1], "http://host/mcp?token=abc123");
}
#[test]
fn test_build_candidate_urls_sse_with_query_params() {
let candidates = build_candidate_urls("http://host/sse?token=abc123&other=1");
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0], "http://host/sse?token=abc123&other=1");
}
#[test]
fn test_build_candidate_urls_sse_no_query() {
let candidates = build_candidate_urls("http://host/sse");
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0], "http://host/sse");
}
#[test]
fn test_build_candidate_urls_no_query() {
let candidates = build_candidate_urls("http://host/mcp");
assert_eq!(candidates.len(), 2);
assert_eq!(candidates[0], "http://host/mcp/sse");
assert_eq!(candidates[1], "http://host/mcp");
}
}