Skip to main content

mcp_streamable_proxy/
fallback.rs

1use anyhow::{Context, Result, bail};
2use rmcp::model::{ListToolsResult, ProtocolVersion, ServerConfig};
3
4#[derive(Clone, Debug)]
5pub struct DiscoverySnapshot {
6    pub server_info: ServerConfig,
7    pub tools: ListToolsResult,
8}
9
10#[derive(Clone, Debug)]
11pub struct FallbackMetadata {
12    snapshot: DiscoverySnapshot,
13}
14
15impl FallbackMetadata {
16    pub fn from_json(initialize: &str, tools: &str) -> Result<Self> {
17        let server_info: ServerConfig = serde_json::from_str(initialize)
18            .context("invalid Streamable HTTP initialize fallback JSON")?;
19        validate_protocol_version(&server_info.protocol_version)?;
20        if server_info.capabilities.tools.is_none() {
21            bail!("Streamable HTTP initialize fallback must declare capabilities.tools");
22        }
23
24        let tools: ListToolsResult =
25            serde_json::from_str(tools).context("invalid Streamable HTTP tools fallback JSON")?;
26        if tools.next_cursor.is_some() {
27            bail!("Streamable HTTP tools fallback must be a complete result without nextCursor");
28        }
29
30        Ok(Self {
31            snapshot: DiscoverySnapshot { server_info, tools },
32        })
33    }
34
35    pub fn snapshot(&self) -> DiscoverySnapshot {
36        self.snapshot.clone()
37    }
38
39    pub fn into_snapshot(self) -> DiscoverySnapshot {
40        self.snapshot
41    }
42}
43
44impl DiscoverySnapshot {
45    pub fn initialize_json(&self) -> Result<String> {
46        serde_json::to_string_pretty(&self.server_info)
47            .context("failed to serialize Streamable HTTP initialize snapshot")
48    }
49
50    pub fn tools_json(&self) -> Result<String> {
51        serde_json::to_string_pretty(&self.tools)
52            .context("failed to serialize Streamable HTTP tools snapshot")
53    }
54}
55
56fn validate_protocol_version(version: &ProtocolVersion) -> Result<()> {
57    if ProtocolVersion::KNOWN_VERSIONS.contains(version) {
58        return Ok(());
59    }
60    bail!("unsupported Streamable HTTP MCP protocol version: {version}")
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    const INITIALIZE: &str = r#"{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"test","version":"1"}}"#;
68    const TOOLS: &str = r#"{"tools":[{"name":"search","inputSchema":{"type":"object"}}]}"#;
69
70    #[test]
71    fn parses_complete_fallback() {
72        let fallback = FallbackMetadata::from_json(INITIALIZE, TOOLS)
73            .expect("valid Streamable HTTP fallback metadata");
74        assert_eq!(fallback.snapshot().tools.tools.len(), 1);
75    }
76
77    #[test]
78    fn rejects_initialize_without_tools_capability() {
79        let initialize = r#"{"protocolVersion":"2025-03-26","capabilities":{},"serverInfo":{"name":"test","version":"1"}}"#;
80        let error = FallbackMetadata::from_json(initialize, TOOLS)
81            .expect_err("missing tools capability must fail");
82        assert!(error.to_string().contains("capabilities.tools"));
83    }
84}