mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
feat: add MCP protocol types and rmcp adapters (#10356)
Currently, types from our custom `mcp-types` crate are part of some of
our APIs:
03fcd12e77/codex-rs/app-server-protocol/src/protocol/v2.rs (L43-L46)
To eliminate this crate in #10349 by switching to `rmcp`, we need our
own wrappers for the `rmcp` types that we can use in our API, which is
what this PR does.
Note this PR introduces the new API types, but we do not make use of
them until #10349.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10356).
* #10357
* #10349
* __->__ #10356
This commit is contained in:
@@ -6,6 +6,7 @@ pub mod config_types;
|
||||
pub mod custom_prompts;
|
||||
pub mod dynamic_tools;
|
||||
pub mod items;
|
||||
pub mod mcp;
|
||||
pub mod message_history;
|
||||
pub mod models;
|
||||
pub mod num_format;
|
||||
|
||||
324
codex-rs/protocol/src/mcp.rs
Normal file
324
codex-rs/protocol/src/mcp.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// 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.
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
pub input_schema: serde_json::Value,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub output_schema: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub annotations: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub icons: Option<Vec<serde_json::Value>>,
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[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_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub mime_type: Option<String>,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[ts(type = "number")]
|
||||
pub size: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
pub uri: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub icons: Option<Vec<serde_json::Value>>,
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[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<serde_json::Value>,
|
||||
pub uri_template: String,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CallToolResult {
|
||||
pub content: Vec<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub structured_content: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub is_error: Option<bool>,
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// === 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<Option<i64>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
match Option::<serde_json::Number>::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<String>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default, rename = "inputSchema", alias = "input_schema")]
|
||||
input_schema: serde_json::Value,
|
||||
#[serde(default, rename = "outputSchema", alias = "output_schema")]
|
||||
output_schema: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
annotations: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
icons: Option<Vec<serde_json::Value>>,
|
||||
#[serde(rename = "_meta", default)]
|
||||
meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl From<ToolSerde> 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_json::Value>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(rename = "mimeType", alias = "mime_type", default)]
|
||||
mime_type: Option<String>,
|
||||
name: String,
|
||||
#[serde(default, deserialize_with = "deserialize_lossy_opt_i64")]
|
||||
size: Option<i64>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
uri: String,
|
||||
#[serde(default)]
|
||||
icons: Option<Vec<serde_json::Value>>,
|
||||
#[serde(rename = "_meta", default)]
|
||||
meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl From<ResourceSerde> 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_json::Value>,
|
||||
#[serde(rename = "uriTemplate", alias = "uri_template")]
|
||||
uri_template: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(rename = "mimeType", alias = "mime_type", default)]
|
||||
mime_type: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ResourceTemplateSerde> 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<Self, serde_json::Error> {
|
||||
Ok(serde_json::from_value::<ToolSerde>(value)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
|
||||
Ok(serde_json::from_value::<ResourceSerde>(value)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceTemplate {
|
||||
pub fn from_mcp_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
|
||||
Ok(serde_json::from_value::<ResourceTemplateSerde>(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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user