Skip to main content

hanzo_protocol/
mcp.rs

1//! Types used when representing Model Context Protocol (MCP) values inside the
2//! Codex protocol.
3//!
4//! We intentionally keep these types TS/JSON-schema friendly (via `ts-rs` and
5//! `schemars`) so they can be embedded in Codex's own protocol structures.
6use schemars::JsonSchema;
7use serde::Deserialize;
8use serde::Serialize;
9use ts_rs::TS;
10
11/// ID of a request, which can be either a string or an integer.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
13#[serde(untagged)]
14pub enum RequestId {
15    String(String),
16    #[ts(type = "number")]
17    Integer(i64),
18}
19
20impl std::fmt::Display for RequestId {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            RequestId::String(s) => f.write_str(s),
24            RequestId::Integer(i) => i.fmt(f),
25        }
26    }
27}
28
29/// Definition for a tool the client can call.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
31#[serde(rename_all = "camelCase")]
32pub struct Tool {
33    pub name: String,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    #[ts(optional)]
36    pub title: Option<String>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    #[ts(optional)]
39    pub description: Option<String>,
40    pub input_schema: serde_json::Value,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    #[ts(optional)]
43    pub output_schema: Option<serde_json::Value>,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    #[ts(optional)]
46    pub annotations: Option<serde_json::Value>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    #[ts(optional)]
49    pub icons: Option<Vec<serde_json::Value>>,
50    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
51    #[ts(optional)]
52    pub meta: Option<serde_json::Value>,
53}
54
55/// A known resource that the server is capable of reading.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
57#[serde(rename_all = "camelCase")]
58pub struct Resource {
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    #[ts(optional)]
61    pub annotations: Option<serde_json::Value>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    #[ts(optional)]
64    pub description: Option<String>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    #[ts(optional)]
67    pub mime_type: Option<String>,
68    pub name: String,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    #[ts(optional)]
71    #[ts(type = "number")]
72    pub size: Option<i64>,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    #[ts(optional)]
75    pub title: Option<String>,
76    pub uri: String,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    #[ts(optional)]
79    pub icons: Option<Vec<serde_json::Value>>,
80    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
81    #[ts(optional)]
82    pub meta: Option<serde_json::Value>,
83}
84
85/// A template description for resources available on the server.
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
87#[serde(rename_all = "camelCase")]
88pub struct ResourceTemplate {
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    #[ts(optional)]
91    pub annotations: Option<serde_json::Value>,
92    pub uri_template: String,
93    pub name: String,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    #[ts(optional)]
96    pub title: Option<String>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    #[ts(optional)]
99    pub description: Option<String>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    #[ts(optional)]
102    pub mime_type: Option<String>,
103}
104
105/// The server's response to a tool call.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
107#[serde(rename_all = "camelCase")]
108pub struct CallToolResult {
109    pub content: Vec<serde_json::Value>,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    #[ts(optional)]
112    pub structured_content: Option<serde_json::Value>,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    #[ts(optional)]
115    pub is_error: Option<bool>,
116    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
117    #[ts(optional)]
118    pub meta: Option<serde_json::Value>,
119}
120
121// === Adapter helpers ===
122//
123// These types and conversions intentionally live in `code-protocol` so other crates can convert
124// “wire-shaped” MCP JSON (typically coming from rmcp model structs serialized with serde) into our
125// TS/JsonSchema-friendly protocol types without depending on `mcp-types`.
126
127fn deserialize_lossy_opt_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
128where
129    D: serde::Deserializer<'de>,
130{
131    match Option::<serde_json::Number>::deserialize(deserializer)? {
132        Some(number) => {
133            if let Some(v) = number.as_i64() {
134                Ok(Some(v))
135            } else if let Some(v) = number.as_u64() {
136                Ok(i64::try_from(v).ok())
137            } else {
138                Ok(None)
139            }
140        }
141        None => Ok(None),
142    }
143}
144
145#[derive(Debug, Deserialize)]
146#[serde(rename_all = "camelCase")]
147struct ToolSerde {
148    name: String,
149    #[serde(default)]
150    title: Option<String>,
151    #[serde(default)]
152    description: Option<String>,
153    #[serde(default, rename = "inputSchema", alias = "input_schema")]
154    input_schema: serde_json::Value,
155    #[serde(default, rename = "outputSchema", alias = "output_schema")]
156    output_schema: Option<serde_json::Value>,
157    #[serde(default)]
158    annotations: Option<serde_json::Value>,
159    #[serde(default)]
160    icons: Option<Vec<serde_json::Value>>,
161    #[serde(rename = "_meta", default)]
162    meta: Option<serde_json::Value>,
163}
164
165impl From<ToolSerde> for Tool {
166    fn from(value: ToolSerde) -> Self {
167        let ToolSerde {
168            name,
169            title,
170            description,
171            input_schema,
172            output_schema,
173            annotations,
174            icons,
175            meta,
176        } = value;
177        Self {
178            name,
179            title,
180            description,
181            input_schema,
182            output_schema,
183            annotations,
184            icons,
185            meta,
186        }
187    }
188}
189
190#[derive(Debug, Deserialize)]
191#[serde(rename_all = "camelCase")]
192struct ResourceSerde {
193    #[serde(default)]
194    annotations: Option<serde_json::Value>,
195    #[serde(default)]
196    description: Option<String>,
197    #[serde(rename = "mimeType", alias = "mime_type", default)]
198    mime_type: Option<String>,
199    name: String,
200    #[serde(default, deserialize_with = "deserialize_lossy_opt_i64")]
201    size: Option<i64>,
202    #[serde(default)]
203    title: Option<String>,
204    uri: String,
205    #[serde(default)]
206    icons: Option<Vec<serde_json::Value>>,
207    #[serde(rename = "_meta", default)]
208    meta: Option<serde_json::Value>,
209}
210
211impl From<ResourceSerde> for Resource {
212    fn from(value: ResourceSerde) -> Self {
213        let ResourceSerde {
214            annotations,
215            description,
216            mime_type,
217            name,
218            size,
219            title,
220            uri,
221            icons,
222            meta,
223        } = value;
224        Self {
225            annotations,
226            description,
227            mime_type,
228            name,
229            size,
230            title,
231            uri,
232            icons,
233            meta,
234        }
235    }
236}
237
238#[derive(Debug, Deserialize)]
239#[serde(rename_all = "camelCase")]
240struct ResourceTemplateSerde {
241    #[serde(default)]
242    annotations: Option<serde_json::Value>,
243    #[serde(rename = "uriTemplate", alias = "uri_template")]
244    uri_template: String,
245    name: String,
246    #[serde(default)]
247    title: Option<String>,
248    #[serde(default)]
249    description: Option<String>,
250    #[serde(rename = "mimeType", alias = "mime_type", default)]
251    mime_type: Option<String>,
252}
253
254impl From<ResourceTemplateSerde> for ResourceTemplate {
255    fn from(value: ResourceTemplateSerde) -> Self {
256        let ResourceTemplateSerde {
257            annotations,
258            uri_template,
259            name,
260            title,
261            description,
262            mime_type,
263        } = value;
264        Self {
265            annotations,
266            uri_template,
267            name,
268            title,
269            description,
270            mime_type,
271        }
272    }
273}
274
275impl Tool {
276    pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
277        Ok(serde_json::from_value::<ToolSerde>(value)?.into())
278    }
279}
280
281impl Resource {
282    pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
283        Ok(serde_json::from_value::<ResourceSerde>(value)?.into())
284    }
285}
286
287impl ResourceTemplate {
288    pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
289        Ok(serde_json::from_value::<ResourceTemplateSerde>(value)?.into())
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use pretty_assertions::assert_eq;
296
297    use super::*;
298
299    #[test]
300    fn resource_size_deserializes_without_narrowing() {
301        let resource = serde_json::json!({
302            "name": "big",
303            "uri": "file:///tmp/big",
304            "size": 5_000_000_000u64,
305        });
306
307        let parsed = Resource::from_mcp_value(resource).expect("should deserialize");
308        assert_eq!(parsed.size, Some(5_000_000_000));
309
310        let resource = serde_json::json!({
311            "name": "negative",
312            "uri": "file:///tmp/negative",
313            "size": -1,
314        });
315
316        let parsed = Resource::from_mcp_value(resource).expect("should deserialize");
317        assert_eq!(parsed.size, Some(-1));
318
319        let resource = serde_json::json!({
320            "name": "too_big_for_i64",
321            "uri": "file:///tmp/too_big_for_i64",
322            "size": 18446744073709551615u64,
323        });
324
325        let parsed = Resource::from_mcp_value(resource).expect("should deserialize");
326        assert_eq!(parsed.size, None);
327    }
328}