Skip to main content

mcp_streamable_proxy/
detector.rs

1use reqwest::header::{ACCEPT, CONTENT_TYPE, HeaderMap, HeaderValue};
2use std::time::Duration;
3
4/// Detect if a URL supports the Streamable HTTP protocol
5///
6/// This detection works by sending an MCP Initialize request
7/// and checking the response characteristics
8///
9/// # Detection characteristics
10///
11/// - Presence of `mcp-session-id` response header (Streamable HTTP specific)
12/// - Valid JSON-RPC 2.0 response format
13/// - POST request returning `text/event-stream` (Streamable HTTP feature)
14///
15/// # Arguments
16///
17/// * `url` - The URL to test
18///
19/// # Returns
20///
21/// Returns `true` if the URL supports Streamable HTTP protocol, `false` otherwise.
22///
23/// # Example
24///
25/// ```rust,ignore
26/// use mcp_streamable_proxy::is_streamable_http;
27///
28/// if is_streamable_http("http://localhost:8080/mcp").await {
29///     println!("Server supports Streamable HTTP");
30/// }
31/// ```
32pub async fn is_streamable_http(url: &str) -> bool {
33    // Build HTTP client with timeout
34    let client = match reqwest::Client::builder()
35        .timeout(Duration::from_secs(5))
36        .build()
37    {
38        Ok(c) => c,
39        Err(_) => return false,
40    };
41
42    // Construct headers for Streamable HTTP detection
43    let mut headers = HeaderMap::new();
44    headers.insert(
45        ACCEPT,
46        HeaderValue::from_static("application/json, text/event-stream"),
47    );
48    headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
49
50    // Construct an MCP Initialize request using rmcp 1.1.0 types
51    use rmcp::model::{
52        ClientCapabilities, ClientRequest, Implementation, InitializeRequestParams,
53        ProtocolVersion, Request, RequestId,
54    };
55
56    let init_request = ClientRequest::InitializeRequest(Request::new(
57        InitializeRequestParams::new(
58            ClientCapabilities::default(),
59            Implementation::new("mcp-proxy-detector", "0.1.0"),
60        )
61        .with_protocol_version(ProtocolVersion::V_2024_11_05),
62    ));
63
64    // Serialize to JSON-RPC message
65    let body = rmcp::model::ClientJsonRpcMessage::request(init_request, RequestId::Number(1));
66
67    // Send POST request and analyze response
68    let response = match client.post(url).headers(headers).json(&body).send().await {
69        Ok(r) => r,
70        Err(_) => return false,
71    };
72
73    let status = response.status();
74    let resp_headers = response.headers().clone();
75
76    // Check 1: Presence of mcp-session-id header (Streamable HTTP specific)
77    if resp_headers.contains_key("mcp-session-id") {
78        return true;
79    }
80    // Check 2: POST request returning text/event-stream (Streamable HTTP feature)
81    if let Some(content_type) = resp_headers.get(CONTENT_TYPE)
82        && let Ok(ct) = content_type.to_str()
83        && ct.contains("text/event-stream")
84        && status.is_success()
85    {
86        return true;
87    }
88    // Check 3: Valid JSON-RPC 2.0 response (even if status is not 2xx)
89    if let Ok(json) = response.json::<serde_json::Value>().await {
90        // JSON-RPC 2.0 response must have jsonrpc: "2.0" field
91        let is_jsonrpc = json
92            .get("jsonrpc")
93            .and_then(|v| v.as_str())
94            .map(|v| v == "2.0")
95            .unwrap_or(false);
96        if is_jsonrpc {
97            return true;
98        }
99    }
100
101    // Check 4: 406 Not Acceptable might indicate Streamable HTTP expecting specific headers
102    if status == reqwest::StatusCode::NOT_ACCEPTABLE {
103        return true;
104    }
105
106    false
107}