Skip to main content

codex_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/// Presentation metadata advertised by an initialized MCP server.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
31#[serde(rename_all = "camelCase")]
32pub struct McpServerInfo {
33    pub name: String,
34    pub title: Option<String>,
35    pub version: String,
36    pub description: Option<String>,
37    pub icons: Option<Vec<serde_json::Value>>,
38    pub website_url: Option<String>,
39}
40
41/// Definition for a tool the client can call.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
43#[serde(rename_all = "camelCase")]
44pub struct Tool {
45    pub name: String,
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    #[ts(optional)]
48    pub title: Option<String>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    #[ts(optional)]
51    pub description: Option<String>,
52    pub input_schema: serde_json::Value,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    #[ts(optional)]
55    pub output_schema: Option<serde_json::Value>,
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    #[ts(optional)]
58    pub annotations: Option<serde_json::Value>,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    #[ts(optional)]
61    pub icons: Option<Vec<serde_json::Value>>,
62    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
63    #[ts(optional)]
64    pub meta: Option<serde_json::Value>,
65}
66
67/// A known resource that the server is capable of reading.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
69#[serde(rename_all = "camelCase")]
70pub struct Resource {
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    #[ts(optional)]
73    pub annotations: Option<serde_json::Value>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    #[ts(optional)]
76    pub description: Option<String>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    #[ts(optional)]
79    pub mime_type: Option<String>,
80    pub name: String,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    #[ts(optional)]
83    #[ts(type = "number")]
84    pub size: Option<i64>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    #[ts(optional)]
87    pub title: Option<String>,
88    pub uri: String,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    #[ts(optional)]
91    pub icons: Option<Vec<serde_json::Value>>,
92    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
93    #[ts(optional)]
94    pub meta: Option<serde_json::Value>,
95}
96
97/// Contents returned when reading a resource from an MCP server.
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
99#[serde(untagged)]
100pub enum ResourceContent {
101    #[serde(rename_all = "camelCase")]
102    #[ts(rename_all = "camelCase")]
103    Text {
104        /// The URI of this resource.
105        uri: String,
106        #[serde(default, skip_serializing_if = "Option::is_none")]
107        #[ts(optional)]
108        mime_type: Option<String>,
109        text: String,
110        #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
111        #[ts(optional)]
112        meta: Option<serde_json::Value>,
113    },
114    #[serde(rename_all = "camelCase")]
115    #[ts(rename_all = "camelCase")]
116    Blob {
117        /// The URI of this resource.
118        uri: String,
119        #[serde(default, skip_serializing_if = "Option::is_none")]
120        #[ts(optional)]
121        mime_type: Option<String>,
122        blob: String,
123        #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
124        #[ts(optional)]
125        meta: Option<serde_json::Value>,
126    },
127}
128
129/// A template description for resources available on the server.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
131#[serde(rename_all = "camelCase")]
132pub struct ResourceTemplate {
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    #[ts(optional)]
135    pub annotations: Option<serde_json::Value>,
136    pub uri_template: String,
137    pub name: String,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    #[ts(optional)]
140    pub title: Option<String>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    #[ts(optional)]
143    pub description: Option<String>,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    #[ts(optional)]
146    pub mime_type: Option<String>,
147}
148
149/// The server's response to a tool call.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
151#[serde(rename_all = "camelCase")]
152pub struct CallToolResult {
153    pub content: Vec<serde_json::Value>,
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    #[ts(optional)]
156    pub structured_content: Option<serde_json::Value>,
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    #[ts(optional)]
159    pub is_error: Option<bool>,
160    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
161    #[ts(optional)]
162    pub meta: Option<serde_json::Value>,
163}
164
165// === Adapter helpers ===
166//
167// These types and conversions intentionally live in `codex-protocol` so other crates can convert
168// “wire-shaped” MCP JSON (typically coming from rmcp model structs serialized with serde) into our
169// TS/JsonSchema-friendly protocol types without depending on `mcp-types`.
170
171fn deserialize_lossy_opt_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
172where
173    D: serde::Deserializer<'de>,
174{
175    match Option::<serde_json::Number>::deserialize(deserializer)? {
176        Some(number) => {
177            if let Some(v) = number.as_i64() {
178                Ok(Some(v))
179            } else if let Some(v) = number.as_u64() {
180                Ok(i64::try_from(v).ok())
181            } else {
182                Ok(None)
183            }
184        }
185        None => Ok(None),
186    }
187}
188
189#[derive(Debug, Deserialize)]
190#[serde(rename_all = "camelCase")]
191struct ToolSerde {
192    name: String,
193    #[serde(default)]
194    title: Option<String>,
195    #[serde(default)]
196    description: Option<String>,
197    #[serde(default, rename = "inputSchema", alias = "input_schema")]
198    input_schema: serde_json::Value,
199    #[serde(default, rename = "outputSchema", alias = "output_schema")]
200    output_schema: Option<serde_json::Value>,
201    #[serde(default)]
202    annotations: Option<serde_json::Value>,
203    #[serde(default)]
204    icons: Option<Vec<serde_json::Value>>,
205    #[serde(rename = "_meta", default)]
206    meta: Option<serde_json::Value>,
207}
208
209impl From<ToolSerde> for Tool {
210    fn from(value: ToolSerde) -> Self {
211        let ToolSerde {
212            name,
213            title,
214            description,
215            input_schema,
216            output_schema,
217            annotations,
218            icons,
219            meta,
220        } = value;
221        Self {
222            name,
223            title,
224            description,
225            input_schema,
226            output_schema,
227            annotations,
228            icons,
229            meta,
230        }
231    }
232}
233
234#[derive(Debug, Deserialize)]
235#[serde(rename_all = "camelCase")]
236struct ResourceSerde {
237    #[serde(default)]
238    annotations: Option<serde_json::Value>,
239    #[serde(default)]
240    description: Option<String>,
241    #[serde(rename = "mimeType", alias = "mime_type", default)]
242    mime_type: Option<String>,
243    name: String,
244    #[serde(default, deserialize_with = "deserialize_lossy_opt_i64")]
245    size: Option<i64>,
246    #[serde(default)]
247    title: Option<String>,
248    uri: String,
249    #[serde(default)]
250    icons: Option<Vec<serde_json::Value>>,
251    #[serde(rename = "_meta", default)]
252    meta: Option<serde_json::Value>,
253}
254
255impl From<ResourceSerde> for Resource {
256    fn from(value: ResourceSerde) -> Self {
257        let ResourceSerde {
258            annotations,
259            description,
260            mime_type,
261            name,
262            size,
263            title,
264            uri,
265            icons,
266            meta,
267        } = value;
268        Self {
269            annotations,
270            description,
271            mime_type,
272            name,
273            size,
274            title,
275            uri,
276            icons,
277            meta,
278        }
279    }
280}
281
282#[derive(Debug, Deserialize)]
283#[serde(rename_all = "camelCase")]
284struct ResourceTemplateSerde {
285    #[serde(default)]
286    annotations: Option<serde_json::Value>,
287    #[serde(rename = "uriTemplate", alias = "uri_template")]
288    uri_template: String,
289    name: String,
290    #[serde(default)]
291    title: Option<String>,
292    #[serde(default)]
293    description: Option<String>,
294    #[serde(rename = "mimeType", alias = "mime_type", default)]
295    mime_type: Option<String>,
296}
297
298impl From<ResourceTemplateSerde> for ResourceTemplate {
299    fn from(value: ResourceTemplateSerde) -> Self {
300        let ResourceTemplateSerde {
301            annotations,
302            uri_template,
303            name,
304            title,
305            description,
306            mime_type,
307        } = value;
308        Self {
309            annotations,
310            uri_template,
311            name,
312            title,
313            description,
314            mime_type,
315        }
316    }
317}
318
319impl Tool {
320    pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
321        Ok(serde_json::from_value::<ToolSerde>(value)?.into())
322    }
323}
324
325impl Resource {
326    pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
327        Ok(serde_json::from_value::<ResourceSerde>(value)?.into())
328    }
329}
330
331impl ResourceTemplate {
332    pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
333        Ok(serde_json::from_value::<ResourceTemplateSerde>(value)?.into())
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use pretty_assertions::assert_eq;
340
341    use super::*;
342
343    #[test]
344    fn resource_size_deserializes_without_narrowing() {
345        let resource = serde_json::json!({
346            "name": "big",
347            "uri": "file:///tmp/big",
348            "size": 5_000_000_000u64,
349        });
350
351        let parsed = Resource::from_mcp_value(resource).expect("should deserialize");
352        assert_eq!(parsed.size, Some(5_000_000_000));
353
354        let resource = serde_json::json!({
355            "name": "negative",
356            "uri": "file:///tmp/negative",
357            "size": -1,
358        });
359
360        let parsed = Resource::from_mcp_value(resource).expect("should deserialize");
361        assert_eq!(parsed.size, Some(-1));
362
363        let resource = serde_json::json!({
364            "name": "too_big_for_i64",
365            "uri": "file:///tmp/too_big_for_i64",
366            "size": 18446744073709551615u64,
367        });
368
369        let parsed = Resource::from_mcp_value(resource).expect("should deserialize");
370        assert_eq!(parsed.size, None);
371    }
372}