codex-tools: extract MCP schema adapters (#15928)

## Why

`codex-tools` already owns the shared tool input schema model and parser
from the first extraction step, but `core/src/tools/spec.rs` still owned
the MCP-specific adapter that normalizes `rmcp::model::Tool` schemas and
wraps `structuredContent` into the call result output schema.

Keeping that adapter in `codex-core` means the reusable MCP schema path
is still split across crates, and the unit tests for that logic stay
anchored in `codex-core` even though the runtime orchestration does not
need to move yet.

This change takes the next small step by moving the reusable MCP schema
adapter into `codex-tools` while leaving `ResponsesApiTool` assembly in
`codex-core`.

## What changed

- added `tools/src/mcp_tool.rs` and sibling
`tools/src/mcp_tool_tests.rs`
- introduced `ParsedMcpTool`, `parse_mcp_tool()`, and
`mcp_call_tool_result_output_schema()` in `codex-tools`
- updated `core/src/tools/spec.rs` to consume parsed MCP tool parts from
`codex-tools`
- removed the now-redundant MCP schema unit tests from
`core/src/tools/spec_tests.rs`
- expanded `codex-rs/tools/README.md` to describe this second migration
step

## Test plan

- `cargo test -p codex-tools`
- `cargo test -p codex-core --lib tools::spec::`
This commit is contained in:
Michael Bolin
2026-03-26 19:57:26 -07:00
committed by GitHub
parent d838c23867
commit be5afc65d3
8 changed files with 212 additions and 197 deletions

View File

@@ -0,0 +1,120 @@
use super::ParsedMcpTool;
use super::mcp_call_tool_result_output_schema;
use super::parse_mcp_tool;
use crate::JsonSchema;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
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,
execution: None,
icons: None,
meta: None,
}
}
#[test]
fn parse_mcp_tool_inserts_empty_properties() {
let tool = mcp_tool(
"no_props",
"No properties",
serde_json::json!({
"type": "object"
}),
);
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ParsedMcpTool {
description: "No properties".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: mcp_call_tool_result_output_schema(serde_json::json!({})),
}
);
}
#[test]
fn parse_mcp_tool_preserves_top_level_output_schema() {
let mut tool = mcp_tool(
"with_output",
"Has output schema",
serde_json::json!({
"type": "object"
}),
);
tool.output_schema = Some(std::sync::Arc::new(rmcp::model::object(
serde_json::json!({
"properties": {
"result": {
"properties": {
"nested": {}
}
}
},
"required": ["result"]
}),
)));
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ParsedMcpTool {
description: "Has output schema".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: mcp_call_tool_result_output_schema(serde_json::json!({
"properties": {
"result": {
"properties": {
"nested": {}
}
}
},
"required": ["result"]
})),
}
);
}
#[test]
fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
let mut tool = mcp_tool(
"with_enum_output",
"Has enum output schema",
serde_json::json!({
"type": "object"
}),
);
tool.output_schema = Some(std::sync::Arc::new(rmcp::model::object(
serde_json::json!({
"enum": ["ok", "error"]
}),
)));
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ParsedMcpTool {
description: "Has enum output schema".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: mcp_call_tool_result_output_schema(serde_json::json!({
"enum": ["ok", "error"]
})),
}
);
}