Skip to main content

hanzo_mcp/
protocol.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// The protocol revisions this server speaks, newest first.
5///
6/// Initialize is a NEGOTIATION: the client states the revision it wants and the
7/// server answers with the one it will actually use. Answering with a fixed
8/// string is not a negotiation, and a client that requires a newer revision has
9/// no choice but to refuse the connection — which is what happened. `hanzo dev`
10/// asks for 2025-06-18, got 2024-11-05 back, and reported the server as failed
11/// to start. The tools were listed, the binary was on PATH, the server answered
12/// a hand-written handshake perfectly, and no agent could use any of it.
13///
14/// The list is what this server can SERVE, not what exists. Across these three
15/// revisions the shapes it implements — initialize, tools/list, tools/call — are
16/// unchanged; the later ones add capabilities (elicitation, structured content,
17/// resource links) that are optional and that this server does not advertise.
18pub const PROTOCOL_VERSIONS: &[&str] = &["2025-06-18", "2025-03-26", "2024-11-05"];
19
20/// The newest revision this server speaks, and what it answers a client whose
21/// request it cannot honour.
22pub const PROTOCOL_VERSION: &str = PROTOCOL_VERSIONS[0];
23
24/// Resolve the revision to speak with a client that asked for `requested`.
25///
26/// Echo what was asked for when it is one of ours — that is the whole contract,
27/// and it is what lets one server serve both an old client and a new one. A
28/// client that asks for something we do not speak, or names nothing at all, is
29/// answered with our newest: the spec's instruction, and it leaves the decision
30/// to disconnect with the client rather than guessing on its behalf.
31pub fn negotiate_version(requested: Option<&str>) -> &'static str {
32    match requested {
33        Some(want) => PROTOCOL_VERSIONS
34            .iter()
35            .find(|v| **v == want)
36            .copied()
37            .unwrap_or(PROTOCOL_VERSION),
38        None => PROTOCOL_VERSION,
39    }
40}
41
42/// MCP Request structure
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct MCPRequest {
45    pub jsonrpc: String,
46    pub id: Option<Value>,
47    pub method: String,
48    pub params: Option<Value>,
49}
50
51/// MCP Response structure
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct MCPResponse {
54    pub jsonrpc: String,
55    pub id: Option<Value>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub result: Option<Value>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub error: Option<MCPError>,
60}
61
62/// MCP Error structure
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct MCPError {
65    pub code: i64,
66    pub message: String,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub data: Option<Value>,
69}
70
71/// Initialize request params
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct InitializeParams {
74    pub protocol_version: String,
75    pub capabilities: ClientCapabilities,
76    pub client_info: ClientInfo,
77}
78
79/// Client capabilities
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ClientCapabilities {
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub experimental: Option<Value>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub sampling: Option<Value>,
86}
87
88/// Client information
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ClientInfo {
91    pub name: String,
92    pub version: String,
93}
94
95/// Server capabilities
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ServerCapabilities {
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub tools: Option<Value>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub resources: Option<Value>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub prompts: Option<Value>,
104}
105
106/// Tool definition
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ToolDefinition {
109    pub name: String,
110    pub description: String,
111    #[serde(rename = "inputSchema")]
112    pub input_schema: Value,
113}
114
115/// Tool call params
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ToolCallParams {
118    pub name: String,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub arguments: Option<Value>,
121}
122
123/// Content types for responses
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type")]
126pub enum Content {
127    #[serde(rename = "text")]
128    Text { text: String },
129    #[serde(rename = "image")]
130    Image { 
131        data: String,
132        #[serde(rename = "mimeType")]
133        mime_type: String,
134    },
135    #[serde(rename = "resource")]
136    Resource {
137        uri: String,
138        #[serde(rename = "mimeType")]
139        mime_type: Option<String>,
140        text: Option<String>,
141    },
142}
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn echoes_a_revision_we_speak() {
149        // The regression: `hanzo dev` asks for 2025-06-18 and refuses the server
150        // unless it hears 2025-06-18 back. Answering a fixed string made every
151        // agent-facing use of this server fail while every hand-written probe of
152        // it passed.
153        for want in PROTOCOL_VERSIONS {
154            assert_eq!(negotiate_version(Some(want)), *want);
155        }
156    }
157
158    #[test]
159    fn answers_our_newest_when_we_cannot_honour_the_request() {
160        // Not an error: the spec has the server state what it WILL speak and
161        // leaves the client to decide whether that is acceptable.
162        assert_eq!(negotiate_version(Some("1999-01-01")), PROTOCOL_VERSION);
163        assert_eq!(negotiate_version(None), PROTOCOL_VERSION);
164        assert_eq!(PROTOCOL_VERSION, "2025-06-18");
165    }
166
167    #[test]
168    fn the_oldest_revision_is_still_served() {
169        // Clients pinned to the original revision must keep working — the point
170        // of negotiating is serving both, not trading one break for another.
171        assert_eq!(negotiate_version(Some("2024-11-05")), "2024-11-05");
172    }
173}