mcp_streamable_proxy/
fallback.rs1use 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}