mcp-sse-proxy 0.2.4

SSE (Server-Sent Events) proxy implementation for MCP protocol using rmcp 0.10
Documentation
use anyhow::{Context, Result, bail};
use rmcp::model::{ListToolsResult, ProtocolVersion, ServerInfo};

#[derive(Clone, Debug)]
pub struct DiscoverySnapshot {
    pub server_info: ServerInfo,
    pub tools: ListToolsResult,
}

#[derive(Clone, Debug)]
pub struct FallbackMetadata {
    snapshot: DiscoverySnapshot,
}

impl FallbackMetadata {
    pub fn from_json(initialize: &str, tools: &str) -> Result<Self> {
        let server_info: ServerInfo =
            serde_json::from_str(initialize).context("invalid SSE initialize fallback JSON")?;
        validate_protocol_version(&server_info.protocol_version)?;
        if server_info.capabilities.tools.is_none() {
            bail!("SSE initialize fallback must declare capabilities.tools");
        }

        let tools: ListToolsResult =
            serde_json::from_str(tools).context("invalid SSE tools fallback JSON")?;
        if tools.next_cursor.is_some() {
            bail!("SSE tools fallback must be a complete result without nextCursor");
        }

        Ok(Self {
            snapshot: DiscoverySnapshot { server_info, tools },
        })
    }

    pub fn snapshot(&self) -> DiscoverySnapshot {
        self.snapshot.clone()
    }

    pub fn into_snapshot(self) -> DiscoverySnapshot {
        self.snapshot
    }
}

impl DiscoverySnapshot {
    pub fn initialize_json(&self) -> Result<String> {
        serde_json::to_string_pretty(&self.server_info)
            .context("failed to serialize SSE initialize snapshot")
    }

    pub fn tools_json(&self) -> Result<String> {
        serde_json::to_string_pretty(&self.tools).context("failed to serialize SSE tools snapshot")
    }
}

fn validate_protocol_version(version: &ProtocolVersion) -> Result<()> {
    if version == &ProtocolVersion::V_2024_11_05
        || version == &ProtocolVersion::V_2025_03_26
        || version == &ProtocolVersion::V_2025_06_18
    {
        return Ok(());
    }
    bail!("unsupported SSE MCP protocol version: {version}")
}

#[cfg(test)]
mod tests {
    use super::*;

    const INITIALIZE: &str = r#"{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"test","version":"1"}}"#;
    const TOOLS: &str = r#"{"tools":[{"name":"search","inputSchema":{"type":"object"}}]}"#;

    #[test]
    fn parses_complete_fallback() {
        let fallback =
            FallbackMetadata::from_json(INITIALIZE, TOOLS).expect("valid SSE fallback metadata");
        assert_eq!(fallback.snapshot().tools.tools.len(), 1);
    }

    #[test]
    fn rejects_incomplete_tools_snapshot() {
        let error = FallbackMetadata::from_json(INITIALIZE, r#"{"tools":[],"nextCursor":"next"}"#)
            .expect_err("cursor must be rejected");
        assert!(error.to_string().contains("complete result"));
    }
}