//! 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 std::collections::HashMap; use ts_rs::TS; /// Observed state of an MCP connection in a published thread runtime. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub enum McpServerConnectionStatus { NotStarted, Starting, Connected, AuthenticationRequired, Failed, Cancelled, Disabled, } /// Extension ID for OpenAI elicitation modes. pub const OPENAI_ELICITATION_EXTENSION_ID: &str = "openai/elicitation"; /// Extension ID for legacy OpenAI form elicitation. pub const OPENAI_FORM_EXTENSION_ID: &str = "openai/form"; /// Extension ID for standard MCP form elicitations that require user-entered input. pub const OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID: &str = "openai/standard-form-input"; /// Extension ID for MCP App UI rendering. pub const MCP_APP_UI_EXTENSION_ID: &str = "io.modelcontextprotocol/ui"; /// Host-supplied confirmation-policy documents for Node REPL-backed actor calls. pub const CONFIRMATION_POLICIES_META_KEY: &str = "openai/confirmation_policies"; /// Returns whether a raw MCP server name identifies a Node REPL-backed server. pub fn is_node_repl_backed_server(server: &str) -> bool { matches!(server, "node_repl" | "cua_repl") } /// Recognizes Node REPL-backed tools in model-visible MCP namespaces or legacy /// flat tool names. An explicit namespace takes precedence over the tool name. pub fn is_node_repl_backed_tool(name: &str, namespace: Option<&str>) -> bool { if let Some(namespace) = namespace { let namespace = namespace.strip_prefix("mcp__").unwrap_or(namespace); return is_node_repl_backed_server(namespace.strip_suffix("__").unwrap_or(namespace)); } let name = name.strip_prefix("mcp__").unwrap_or(name); name.split_once("__") .is_some_and(|(server, _)| is_node_repl_backed_server(server)) } /// Bounded app-resource provenance retained across a compaction checkpoint. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct McpResourceOriginCheckpoint { pub origins: Vec, pub turns: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub current_turn_id: Option, } /// The original app, account, tool, and URI that authorize one widget read. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct McpResourceOrigin { pub call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub turn_id: Option, pub tool: String, pub connector_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub link_id: Option, pub uri: String, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub ambiguous_account: bool, } /// Client extensions that must not be advertised to MCP servers. const MCP_CLIENT_ONLY_EXTENSION_IDS: [&str; 1] = [OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID]; /// MCP extensions supplied by the client that created a Codex session. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct ClientMcpExtensions { extensions: HashMap, } impl ClientMcpExtensions { /// Creates a session extension set from trusted client declarations. pub fn new(extensions: impl IntoIterator) -> Self { Self { extensions: extensions.into_iter().collect(), } } /// Returns whether the client declared the given extension. pub fn contains(&self, extension_id: &str) -> bool { self.extensions.contains_key(extension_id) } /// Returns the client's settings for the given extension. pub fn get(&self, extension_id: &str) -> Option<&serde_json::Value> { self.extensions.get(extension_id) } /// Returns only client extensions that should be advertised to MCP servers. pub fn for_mcp_servers(&self) -> Self { Self::new( self.extensions .iter() .filter(|(id, _)| !MCP_CLIENT_ONLY_EXTENSION_IDS.contains(&id.as_str())) .map(|(id, settings)| (id.clone(), settings.clone())), ) } /// Iterates over the extensions and their settings. pub fn iter(&self) -> impl Iterator { self.extensions .iter() .map(|(id, settings)| (id.as_str(), settings)) } } /// 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), } } } /// Presentation metadata advertised by an initialized MCP server. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct McpServerInfo { pub name: String, pub title: Option, pub version: String, pub description: Option, pub icons: Option>, pub website_url: Option, } /// 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, } /// Contents returned when reading a resource from an MCP server. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] pub enum ResourceContent { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] Text { /// The URI of this resource. uri: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] mime_type: Option, text: String, #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] #[ts(optional)] meta: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] Blob { /// The URI of this resource. uri: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] mime_type: Option, blob: String, #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] #[ts(optional)] 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 node_repl_backed_tool_recognizes_namespaces_and_legacy_names() { for (name, namespace, expected) in [ ("read_file", Some("mcp__node_repl"), true), ("read_file", Some("mcp__node_repl__"), true), ("read_file", Some("node_repl"), true), ("read_file", Some("node_repl__"), true), ("read_file", Some("mcp__cua_repl"), true), ("read_file", Some("mcp__cua_repl__"), true), ("read_file", Some("cua_repl"), true), ("read_file", Some("cua_repl__"), true), ("read_file", Some("node_repl_"), false), ("read_file", Some("cua_repl_"), false), ("read_file", Some("mcp__node_repl____"), false), ("read_file", Some("mcp__cua_repl____"), false), ("mcp__node_repl__js", None, true), ("node_repl__js", None, true), ("mcp__cua_repl__js", None, true), ("cua_repl__js", None, true), ("mcp__node_repl__js", Some("other"), false), ("mcp__cua_repl__js", Some("other"), false), ("mcp__node_repl_other__js", None, false), ("mcp__cua_repl_other__js", None, false), ] { assert_eq!( is_node_repl_backed_tool(name, namespace), expected, "tool {name} in namespace {namespace:?}" ); } } #[test] fn client_mcp_extensions_for_mcp_servers_excludes_client_only_extensions() { let openai_form_settings = serde_json::json!({ "version": 1 }); let app_ui_settings = serde_json::json!({ "mimeTypes": ["text/html"] }); let future_server_extension_settings = serde_json::json!({ "version": 2 }); let extensions = ClientMcpExtensions::new([ ( OPENAI_FORM_EXTENSION_ID.to_string(), openai_form_settings.clone(), ), (MCP_APP_UI_EXTENSION_ID.to_string(), app_ui_settings.clone()), ( OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID.to_string(), serde_json::json!({}), ), ( "example/future-server-extension".to_string(), future_server_extension_settings.clone(), ), ]); assert_eq!( extensions.for_mcp_servers(), ClientMcpExtensions::new([ (OPENAI_FORM_EXTENSION_ID.to_string(), openai_form_settings), (MCP_APP_UI_EXTENSION_ID.to_string(), app_ui_settings), ( "example/future-server-extension".to_string(), future_server_extension_settings, ), ]) ); } #[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); } }