Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/codex-mcp/src/codex_apps_cache_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
format!("Test tool: {tool_name}"),
Arc::new(JsonObject::default()),
),
openai_file_input_optional_fields: Default::default(),
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/codex-mcp/src/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::runtime::emit_duration;
use crate::server::EffectiveMcpServer;
use crate::server::McpServerMetadata;
use crate::tools::ToolInfo;
use crate::tools::declared_openai_file_input_optional_fields;
use crate::tools::filter_tools;
use crate::tools::normalize_tools_for_model_with_prefix;
use crate::tools::tool_with_model_visible_input_schema;
Expand Down Expand Up @@ -579,6 +580,8 @@ impl McpConnectionManager {
let tools = filter_tools(tools, &managed_client.tool_filter)
.into_iter()
.map(|mut tool| {
tool.openai_file_input_optional_fields =
declared_openai_file_input_optional_fields(&tool.tool);
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
self.with_server_metadata(tool)
});
Expand Down
62 changes: 62 additions & 0 deletions codex-rs/codex-mcp/src/connection_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::server::McpServerMetadata;
use crate::server::McpServerOrigin;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::declared_openai_file_input_optional_fields;
use crate::tools::filter_tools;
use crate::tools::normalize_tools_for_model_with_prefix;
use crate::tools::tool_with_model_visible_input_schema;
Expand All @@ -43,6 +44,7 @@ use rmcp::model::JsonObject;
use rmcp::model::Meta;
use rmcp::model::NumberOrString;
use rmcp::model::Tool;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::sync::Arc;
Expand All @@ -63,6 +65,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
format!("Test tool: {tool_name}"),
Arc::new(JsonObject::default()),
),
openai_file_input_optional_fields: Default::default(),
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
Expand Down Expand Up @@ -276,6 +279,65 @@ fn tool_with_model_visible_input_schema_masks_file_params() {
);
}

#[test]
fn declared_openai_file_input_optional_fields_follows_payload_schema() {
let mut tool = Tool::new(
"upload".to_string(),
"Upload a file".to_string(),
Arc::new(
serde_json::json!({
"type": "object",
"properties": {
"file": {
"type": "object",
"properties": {
"download_url": {"type": "string"},
"file_id": {"type": "string"},
"mime_type": {"type": "string"},
"file_name": {"type": "string"},
"uri": {"type": "string"},
"file_size_bytes": {"type": "integer"}
}
},
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"download_url": {"type": "string"},
"file_id": {"type": "string"},
"mime_type": {"type": "string"}
}
}
}
}
})
.as_object()
.expect("object")
.clone(),
),
);
tool.meta = Some(Meta(
serde_json::json!({
"openai/fileParams": ["file", "files"]
})
.as_object()
.expect("object")
.clone(),
));

assert_eq!(
declared_openai_file_input_optional_fields(&tool),
HashMap::from([
(
"file".to_string(),
vec!["mime_type".to_string(), "file_name".to_string()]
),
("files".to_string(), vec!["mime_type".to_string()]),
])
);
}

#[test]
fn tool_with_model_visible_input_schema_leaves_tools_without_file_params_unchanged() {
let original_tool = create_test_tool("custom", "upload").tool;
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/codex-mcp/src/rmcp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use crate::server::EffectiveMcpServer;
use crate::server::McpServerLaunch;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::declared_openai_file_input_optional_fields;
use crate::tools::filter_tools;
use crate::tools::tool_with_model_visible_input_schema;
use anyhow::Result;
Expand Down Expand Up @@ -594,6 +595,8 @@ fn prepare_codex_apps_tools_for_model(
tool_plugin_provenance: &ToolPluginProvenance,
) -> Vec<ToolInfo> {
for tool in &mut tools {
tool.openai_file_input_optional_fields =
declared_openai_file_input_optional_fields(&tool.tool);
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
let plugin_names = match tool.connector_id.as_deref() {
Some(connector_id) => {
Expand Down Expand Up @@ -707,6 +710,7 @@ fn codex_apps_tool_info_from_listed_tool(
callable_namespace,
namespace_description,
tool: tool_def,
openai_file_input_optional_fields: HashMap::new(),
connector_id,
connector_name,
plugin_display_names: Vec::new(),
Expand All @@ -730,6 +734,7 @@ fn regular_mcp_tool_info_from_listed_tool(
callable_namespace: server_name.to_string(),
namespace_description: server_instructions.map(str::to_string),
tool: tool_def,
openai_file_input_optional_fields: HashMap::new(),
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
Expand Down Expand Up @@ -1117,6 +1122,7 @@ mod tests {
callable_namespace: "codex_apps__gmail".to_string(),
namespace_description: Some("Mail connector".to_string()),
tool: expected_tool,
openai_file_input_optional_fields: HashMap::new(),
connector_id: Some("connector_gmail".to_string()),
connector_name: Some("Gmail".to_string()),
plugin_display_names: Vec::new(),
Expand Down
47 changes: 47 additions & 0 deletions codex-rs/codex-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ pub struct ToolInfo {
pub namespace_description: Option<String>,
/// Raw MCP tool definition; `tool.name` is sent back to the MCP server.
pub tool: Tool,
/// Optional provided-file fields accepted by each declared `openai/fileParams`
/// argument. This is derived from the raw MCP schema before file arguments are
/// masked as local paths for the model.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub openai_file_input_optional_fields: HashMap<String, Vec<String>>,
pub connector_id: Option<String>,
pub connector_name: Option<String>,
#[serde(default)]
Expand Down Expand Up @@ -261,6 +266,48 @@ const MCP_TOOL_NAME_DELIMITER: &str = "__";
const MAX_TOOL_NAME_LENGTH: usize = 64;
const CALLABLE_NAME_HASH_LEN: usize = 12;
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
const OPTIONAL_OPENAI_FILE_FIELDS: [&str; 2] = ["mime_type", "file_name"];

pub(crate) fn declared_openai_file_input_optional_fields(
tool: &Tool,
) -> HashMap<String, Vec<String>> {
let file_params = declared_openai_file_input_param_names(tool.meta.as_deref());
let properties = tool
.input_schema
.get("properties")
.and_then(JsonValue::as_object);

file_params
.into_iter()
.map(|field_name| {
let optional_fields = properties
.and_then(|properties| properties.get(&field_name))
.and_then(file_payload_schema)
.and_then(|schema| schema.get("properties"))
.and_then(JsonValue::as_object)
.map(|properties| {
OPTIONAL_OPENAI_FILE_FIELDS
.into_iter()
.filter(|field| properties.contains_key(*field))
.map(str::to_string)
.collect()
})
.unwrap_or_default();
(field_name, optional_fields)
})
.collect()
}

fn file_payload_schema(schema: &JsonValue) -> Option<&serde_json::Map<String, JsonValue>> {
let schema = schema.as_object()?;
if schema.get("type").and_then(JsonValue::as_str) == Some("array")
|| schema.contains_key("items")
{
schema.get("items")?.as_object()
} else {
Some(schema)
}
}

fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) {
let Some(properties) = input_schema
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/src/connectors_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fn codex_app_tool(
callable_namespace: tool_namespace,
namespace_description: None,
tool: test_tool_definition(tool_name),
openai_file_input_optional_fields: Default::default(),
connector_id: Some(connector_id.to_string()),
connector_name: connector_name.map(ToOwned::to_owned),
plugin_display_names: plugin_names(plugin_display_names),
Expand Down Expand Up @@ -115,6 +116,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
callable_namespace: "sample".to_string(),
namespace_description: None,
tool: test_tool_definition("echo"),
openai_file_input_optional_fields: Default::default(),
connector_id: None,
connector_name: None,
plugin_display_names: plugin_names(&["ignored"]),
Expand Down Expand Up @@ -297,6 +299,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
"Create a calendar event",
Arc::new(JsonObject::default()),
),
openai_file_input_optional_fields: Default::default(),
connector_id: Some("calendar".to_string()),
connector_name: Some("Calendar".to_string()),
plugin_display_names: Vec::new(),
Expand Down
Loading
Loading