mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
feat: replace custom mcp-types crate with equivalents from rmcp (#10349)
We started working with MCP in Codex before
https://crates.io/crates/rmcp was mature, so we had our own crate for
MCP types that was generated from the MCP schema:
8b95d3e082/codex-rs/mcp-types/README.md
Now that `rmcp` is more mature, it makes more sense to use their MCP
types in Rust, as they handle details (like the `_meta` field) that our
custom version ignored. Though one advantage that our custom types had
is that our generated types implemented `JsonSchema` and `ts_rs::TS`,
whereas the types in `rmcp` do not. As such, part of the work of this PR
is leveraging the adapters between `rmcp` types and the serializable
types that are API for us (app server and MCP) introduced in #10356.
Note this PR results in a number of changes to
`codex-rs/app-server-protocol/schema`, which merit special attention
during review. We must ensure that these changes are still
backwards-compatible, which is possible because we have:
```diff
- export type CallToolResult = { content: Array<ContentBlock>, isError?: boolean, structuredContent?: JsonValue, };
+ export type CallToolResult = { content: Array<JsonValue>, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, };
```
so `ContentBlock` has been replaced with the more general `JsonValue`.
Note that `ContentBlock` was defined as:
```typescript
export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource;
```
so the deletion of those individual variants should not be a cause of
great concern.
Similarly, we have the following change in
`codex-rs/app-server-protocol/schema/typescript/Tool.ts`:
```
- export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, };
+ export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array<JsonValue>, _meta?: JsonValue, };
```
so:
- `annotations?: ToolAnnotations` ➡️ `JsonValue`
- `inputSchema: ToolInputSchema` ➡️ `JsonValue`
- `outputSchema?: ToolOutputSchema` ➡️ `JsonValue`
and two new fields: `icons?: Array<JsonValue>, _meta?: JsonValue`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10349).
* #10357
* __->__ #10349
* #10356
This commit is contained in:
@@ -1087,20 +1087,26 @@ pub(crate) fn create_tools_json_for_chat_completions_api(
|
||||
|
||||
pub(crate) fn mcp_tool_to_openai_tool(
|
||||
fully_qualified_name: String,
|
||||
tool: mcp_types::Tool,
|
||||
tool: rmcp::model::Tool,
|
||||
) -> Result<ResponsesApiTool, serde_json::Error> {
|
||||
let mcp_types::Tool {
|
||||
let rmcp::model::Tool {
|
||||
description,
|
||||
mut input_schema,
|
||||
input_schema,
|
||||
..
|
||||
} = tool;
|
||||
|
||||
// OpenAI models mandate the "properties" field in the schema. The Agents
|
||||
// SDK fixed this by inserting an empty object for "properties" if it is not
|
||||
// already present https://github.com/openai/openai-agents-python/issues/449
|
||||
// so here we do the same.
|
||||
if input_schema.properties.is_none() {
|
||||
input_schema.properties = Some(serde_json::Value::Object(serde_json::Map::new()));
|
||||
let mut serialized_input_schema = serde_json::Value::Object(input_schema.as_ref().clone());
|
||||
|
||||
// OpenAI models mandate the "properties" field in the schema. Some MCP
|
||||
// servers omit it (or set it to null), so we insert an empty object to
|
||||
// match the behavior of the Agents SDK.
|
||||
if let serde_json::Value::Object(obj) = &mut serialized_input_schema
|
||||
&& obj.get("properties").is_none_or(serde_json::Value::is_null)
|
||||
{
|
||||
obj.insert(
|
||||
"properties".to_string(),
|
||||
serde_json::Value::Object(serde_json::Map::new()),
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize to a raw JSON value so we can sanitize schemas coming from MCP
|
||||
@@ -1108,13 +1114,12 @@ pub(crate) fn mcp_tool_to_openai_tool(
|
||||
// Schemas (e.g. using enum/anyOf), or use unsupported variants like
|
||||
// `integer`. Our internal JsonSchema is a small subset and requires
|
||||
// `type`, so we coerce/sanitize here for compatibility.
|
||||
let mut serialized_input_schema = serde_json::to_value(input_schema)?;
|
||||
sanitize_json_schema(&mut serialized_input_schema);
|
||||
let input_schema = serde_json::from_value::<JsonSchema>(serialized_input_schema)?;
|
||||
|
||||
Ok(ResponsesApiTool {
|
||||
name: fully_qualified_name,
|
||||
description: description.unwrap_or_default(),
|
||||
description: description.map(Into::into).unwrap_or_default(),
|
||||
strict: false,
|
||||
parameters: input_schema,
|
||||
})
|
||||
@@ -1254,7 +1259,7 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||||
/// Builds the tool registry builder while collecting tool specs for later serialization.
|
||||
pub(crate) fn build_specs(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, mcp_types::Tool>>,
|
||||
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
use crate::tools::handlers::ApplyPatchHandler;
|
||||
@@ -1410,7 +1415,7 @@ pub(crate) fn build_specs(
|
||||
}
|
||||
|
||||
if let Some(mcp_tools) = mcp_tools {
|
||||
let mut entries: Vec<(String, mcp_types::Tool)> = mcp_tools.into_iter().collect();
|
||||
let mut entries: Vec<(String, rmcp::model::Tool)> = mcp_tools.into_iter().collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
for (name, tool) in entries.into_iter() {
|
||||
@@ -1452,11 +1457,50 @@ mod tests {
|
||||
use crate::config::test_config;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use crate::tools::registry::ConfiguredToolSpec;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn mcp_tool(
|
||||
name: &str,
|
||||
description: &str,
|
||||
input_schema: serde_json::Value,
|
||||
) -> rmcp::model::Tool {
|
||||
rmcp::model::Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(description.to_string().into()),
|
||||
input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_to_openai_tool_inserts_empty_properties() {
|
||||
let mut schema = rmcp::model::JsonObject::new();
|
||||
schema.insert("type".to_string(), serde_json::json!("object"));
|
||||
|
||||
let tool = rmcp::model::Tool {
|
||||
name: "no_props".to_string().into(),
|
||||
title: None,
|
||||
description: Some("No properties".to_string().into()),
|
||||
input_schema: std::sync::Arc::new(schema),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let openai_tool =
|
||||
mcp_tool_to_openai_tool("server/no_props".to_string(), tool).expect("convert tool");
|
||||
let parameters = serde_json::to_value(openai_tool.parameters).expect("serialize schema");
|
||||
|
||||
assert_eq!(parameters.get("properties"), Some(&serde_json::json!({})));
|
||||
}
|
||||
|
||||
fn tool_name(tool: &ToolSpec) -> &str {
|
||||
match tool {
|
||||
ToolSpec::Function(ResponsesApiTool { name, .. }) => name,
|
||||
@@ -2026,37 +2070,26 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "do_something_cool".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"string_argument": {
|
||||
"type": "string",
|
||||
},
|
||||
"number_argument": {
|
||||
"type": "number",
|
||||
},
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_argument": { "type": "string" },
|
||||
"number_argument": { "type": "number" },
|
||||
"object_argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_property": { "type": "string" },
|
||||
"number_property": { "type": "number" },
|
||||
},
|
||||
"required": [
|
||||
"string_property",
|
||||
"number_property",
|
||||
],
|
||||
"additionalProperties": Some(false),
|
||||
"required": ["string_property", "number_property"],
|
||||
"additionalProperties": false,
|
||||
},
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Do something cool".to_string()),
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2120,51 +2153,18 @@ mod tests {
|
||||
});
|
||||
|
||||
// Intentionally construct a map with keys that would sort alphabetically.
|
||||
let tools_map: HashMap<String, mcp_types::Tool> = HashMap::from([
|
||||
let tools_map: HashMap<String, rmcp::model::Tool> = HashMap::from([
|
||||
(
|
||||
"test_server/do".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "a".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("a".to_string()),
|
||||
},
|
||||
mcp_tool("a", "a", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
(
|
||||
"test_server/something".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "b".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("b".to_string()),
|
||||
},
|
||||
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
(
|
||||
"test_server/cool".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "c".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("c".to_string()),
|
||||
},
|
||||
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -2200,22 +2200,16 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/search".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "search".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"query": {
|
||||
"description": "search query"
|
||||
}
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Search docs".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"search",
|
||||
"Search docs",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"description": "search query"}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2258,20 +2252,14 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/paginate".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "paginate".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"page": { "type": "integer" }
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Pagination".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"paginate",
|
||||
"Pagination",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2313,20 +2301,14 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/tags".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "tags".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"tags": { "type": "array" }
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Tags".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"tags",
|
||||
"Tags",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"tags": {"type": "array"}}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2370,20 +2352,16 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/value".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "value".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"value": { "anyOf": [ { "type": "string" }, { "type": "number" } ] }
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("AnyOf Value".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"value",
|
||||
"AnyOf Value",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2482,46 +2460,33 @@ Examples of valid command strings:
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "do_something_cool".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"string_argument": {
|
||||
"type": "string",
|
||||
},
|
||||
"number_argument": {
|
||||
"type": "number",
|
||||
},
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_argument": {"type": "string"},
|
||||
"number_argument": {"type": "number"},
|
||||
"object_argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_property": { "type": "string" },
|
||||
"number_property": { "type": "number" },
|
||||
"string_property": {"type": "string"},
|
||||
"number_property": {"type": "number"}
|
||||
},
|
||||
"required": [
|
||||
"string_property",
|
||||
"number_property",
|
||||
],
|
||||
"required": ["string_property", "number_property"],
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addtl_prop": { "type": "string" },
|
||||
"addtl_prop": {"type": "string"}
|
||||
},
|
||||
"required": [
|
||||
"addtl_prop",
|
||||
],
|
||||
"additionalProperties": false,
|
||||
},
|
||||
},
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Do something cool".to_string()),
|
||||
},
|
||||
"required": ["addtl_prop"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user