Skip to main content

skiff_cli/mcp/
transport.rs

1//! MCP HTTP transport selection: streamable, legacy SSE, or auto.
2
3use serde_json::{Map, Value};
4
5use crate::error::{Error, Result};
6use crate::mcp::common::McpClient;
7use crate::mcp::http::{call_tool_http, connect_streamable, fetch_mcp_tools_http};
8use crate::mcp::sse::{call_tool_sse, connect_sse, fetch_mcp_tools_sse};
9use crate::oauth::OAuthReady;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TransportMode {
13    Auto,
14    Sse,
15    Streamable,
16}
17
18impl TransportMode {
19    pub fn parse(s: &str) -> Result<Self> {
20        match s {
21            "auto" => Ok(Self::Auto),
22            "sse" => Ok(Self::Sse),
23            "streamable" => Ok(Self::Streamable),
24            other => Err(Error::usage(format!(
25                "invalid --transport {other:?}; expected auto|sse|streamable"
26            ))),
27        }
28    }
29}
30
31pub async fn fetch_mcp_tools(
32    url: &str,
33    auth_headers: &[(String, String)],
34    cache_key: &str,
35    ttl: u64,
36    refresh: bool,
37    transport: TransportMode,
38    oauth: Option<&OAuthReady>,
39) -> Result<Vec<Value>> {
40    match transport {
41        TransportMode::Streamable => {
42            fetch_mcp_tools_http(url, auth_headers, cache_key, ttl, refresh, oauth).await
43        }
44        TransportMode::Sse => {
45            fetch_mcp_tools_sse(url, auth_headers, cache_key, ttl, refresh, oauth).await
46        }
47        TransportMode::Auto => {
48            match fetch_mcp_tools_http(url, auth_headers, cache_key, ttl, refresh, oauth).await {
49                Ok(tools) => Ok(tools),
50                Err(streamable_err) => {
51                    tracing::debug!("streamable HTTP failed, trying SSE: {streamable_err}");
52                    fetch_mcp_tools_sse(url, auth_headers, cache_key, ttl, refresh, oauth)
53                        .await
54                        .map_err(|sse_err| {
55                            Error::runtime(format!(
56                                "MCP HTTP failed (streamable: {streamable_err}; sse: {sse_err})"
57                            ))
58                        })
59                }
60            }
61        }
62    }
63}
64
65pub async fn call_tool(
66    url: &str,
67    auth_headers: &[(String, String)],
68    tool_name: &str,
69    arguments: Map<String, Value>,
70    full_envelope: bool,
71    transport: TransportMode,
72    oauth: Option<&OAuthReady>,
73) -> Result<Value> {
74    match transport {
75        TransportMode::Streamable => {
76            call_tool_http(
77                url,
78                auth_headers,
79                tool_name,
80                arguments,
81                full_envelope,
82                oauth,
83            )
84            .await
85        }
86        TransportMode::Sse => {
87            call_tool_sse(
88                url,
89                auth_headers,
90                tool_name,
91                arguments,
92                full_envelope,
93                oauth,
94            )
95            .await
96        }
97        TransportMode::Auto => {
98            match call_tool_http(
99                url,
100                auth_headers,
101                tool_name,
102                arguments.clone(),
103                full_envelope,
104                oauth,
105            )
106            .await
107            {
108                Ok(v) => Ok(v),
109                Err(streamable_err) => {
110                    tracing::debug!("streamable call failed, trying SSE: {streamable_err}");
111                    call_tool_sse(
112                        url,
113                        auth_headers,
114                        tool_name,
115                        arguments,
116                        full_envelope,
117                        oauth,
118                    )
119                    .await
120                    .map_err(|sse_err| {
121                        Error::runtime(format!(
122                            "MCP call failed (streamable: {streamable_err}; sse: {sse_err})"
123                        ))
124                    })
125                }
126            }
127        }
128    }
129}
130
131/// Open a long-lived HTTP MCP client (for session daemons).
132pub async fn connect_http(
133    url: &str,
134    auth_headers: &[(String, String)],
135    transport: TransportMode,
136    oauth: Option<&OAuthReady>,
137) -> Result<McpClient> {
138    match transport {
139        TransportMode::Streamable => connect_streamable(url, auth_headers, oauth).await,
140        TransportMode::Sse => connect_sse(url, auth_headers, oauth).await,
141        TransportMode::Auto => match connect_streamable(url, auth_headers, oauth).await {
142            Ok(c) => Ok(c),
143            Err(streamable_err) => {
144                tracing::debug!("streamable connect failed, trying SSE: {streamable_err}");
145                connect_sse(url, auth_headers, oauth)
146                    .await
147                    .map_err(|sse_err| {
148                        Error::runtime(format!(
149                            "MCP connect failed (streamable: {streamable_err}; sse: {sse_err})"
150                        ))
151                    })
152            }
153        },
154    }
155}