//! Types used when representing Model Context Protocol (MCP) values inside the //! Codex protocol. //! //! We intentionally keep these types TS/JSON-schema friendly (via `ts-rs` and //! `schemars`) so they can be embedded in Codex's own protocol structures. use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use ts_rs::TS; /// ID of a request, which can be either a string or an integer. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] pub enum RequestId { String(String), #[ts(type = "number")] Integer(i64), } impl std::fmt::Display for RequestId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RequestId::String(s) => f.write_str(s), RequestId::Integer(i) => i.fmt(f), } } } /// Definition for a tool the client can call. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct Tool { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub description: Option, pub input_schema: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub output_schema: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub annotations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub icons: Option>, #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub meta: Option, } /// A known resource that the server is capable of reading. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct Resource { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub annotations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub mime_type: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] #[ts(type = "number")] pub size: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub title: Option, pub uri: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub icons: Option>, #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub meta: Option, } /// A template description for resources available on the server. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ResourceTemplate { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub annotations: Option, pub uri_template: String, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub mime_type: Option, } /// The server's response to a tool call. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct CallToolResult { pub content: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub structured_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub is_error: Option, #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub meta: Option, } // === Adapter helpers === // // These types and conversions intentionally live in `codex-protocol` so other crates can convert // “wire-shaped” MCP JSON (typically coming from rmcp model structs serialized with serde) into our // TS/JsonSchema-friendly protocol types without depending on `mcp-types`. fn deserialize_lossy_opt_i64<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { match Option::::deserialize(deserializer)? { Some(number) => { if let Some(v) = number.as_i64() { Ok(Some(v)) } else if let Some(v) = number.as_u64() { Ok(i64::try_from(v).ok()) } else { Ok(None) } } None => Ok(None), } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ToolSerde { name: String, #[serde(default)] title: Option, #[serde(default)] description: Option, #[serde(default, rename = "inputSchema", alias = "input_schema")] input_schema: serde_json::Value, #[serde(default, rename = "outputSchema", alias = "output_schema")] output_schema: Option, #[serde(default)] annotations: Option, #[serde(default)] icons: Option>, #[serde(rename = "_meta", default)] meta: Option, } impl From for Tool { fn from(value: ToolSerde) -> Self { let ToolSerde { name, title, description, input_schema, output_schema, annotations, icons, meta, } = value; Self { name, title, description, input_schema, output_schema, annotations, icons, meta, } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ResourceSerde { #[serde(default)] annotations: Option, #[serde(default)] description: Option, #[serde(rename = "mimeType", alias = "mime_type", default)] mime_type: Option, name: String, #[serde(default, deserialize_with = "deserialize_lossy_opt_i64")] size: Option, #[serde(default)] title: Option, uri: String, #[serde(default)] icons: Option>, #[serde(rename = "_meta", default)] meta: Option, } impl From for Resource { fn from(value: ResourceSerde) -> Self { let ResourceSerde { annotations, description, mime_type, name, size, title, uri, icons, meta, } = value; Self { annotations, description, mime_type, name, size, title, uri, icons, meta, } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ResourceTemplateSerde { #[serde(default)] annotations: Option, #[serde(rename = "uriTemplate", alias = "uri_template")] uri_template: String, name: String, #[serde(default)] title: Option, #[serde(default)] description: Option, #[serde(rename = "mimeType", alias = "mime_type", default)] mime_type: Option, } impl From for ResourceTemplate { fn from(value: ResourceTemplateSerde) -> Self { let ResourceTemplateSerde { annotations, uri_template, name, title, description, mime_type, } = value; Self { annotations, uri_template, name, title, description, mime_type, } } } impl Tool { pub fn from_mcp_value(value: serde_json::Value) -> Result { Ok(serde_json::from_value::(value)?.into()) } } impl Resource { pub fn from_mcp_value(value: serde_json::Value) -> Result { Ok(serde_json::from_value::(value)?.into()) } } impl ResourceTemplate { pub fn from_mcp_value(value: serde_json::Value) -> Result { Ok(serde_json::from_value::(value)?.into()) } } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use super::*; #[test] fn resource_size_deserializes_without_narrowing() { let resource = serde_json::json!({ "name": "big", "uri": "file:///tmp/big", "size": 5_000_000_000u64, }); let parsed = Resource::from_mcp_value(resource).expect("should deserialize"); assert_eq!(parsed.size, Some(5_000_000_000)); let resource = serde_json::json!({ "name": "negative", "uri": "file:///tmp/negative", "size": -1, }); let parsed = Resource::from_mcp_value(resource).expect("should deserialize"); assert_eq!(parsed.size, Some(-1)); let resource = serde_json::json!({ "name": "too_big_for_i64", "uri": "file:///tmp/too_big_for_i64", "size": 18446744073709551615u64, }); let parsed = Resource::from_mcp_value(resource).expect("should deserialize"); assert_eq!(parsed.size, None); } }