Add output schema to MCP tools and expose MCP tool results in code mode (#14236)

Summary
- drop `McpToolOutput` in favor of `CallToolResult`, moving its helpers
to keep MCP tooling focused on the final result shape
- wire the new schema definitions through code mode, context, handlers,
and spec modules so MCP tools serialize the exact output shape expected
by the model
- extend code mode tests to cover multiple MCP call scenarios and ensure
the serialized data matches the new schema
- refresh JS runner helpers and protocol models alongside the schema
changes

Testing
- Not run (not requested)
This commit is contained in:
pakrym-oai
2026-03-10 15:25:19 -07:00
committed by GitHub
parent 15163050dc
commit c7e28cffab
12 changed files with 659 additions and 137 deletions

View File

@@ -7,10 +7,10 @@ use crate::truncate::TruncationPolicy;
use crate::truncate::formatted_truncate_text;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::unified_exec::resolve_max_tokens;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::McpToolOutput;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ShellToolCallParams;
use codex_protocol::models::function_call_output_content_items_to_text;
@@ -82,7 +82,7 @@ pub trait ToolOutput: Send {
}
}
impl ToolOutput for McpToolOutput {
impl ToolOutput for CallToolResult {
fn log_preview(&self) -> String {
let output = self.as_function_call_output_payload();
let preview = output.body.to_text().unwrap_or_else(|| output.to_string());
@@ -90,7 +90,7 @@ impl ToolOutput for McpToolOutput {
}
fn success_for_logging(&self) -> bool {
self.success
self.success()
}
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
@@ -99,6 +99,12 @@ impl ToolOutput for McpToolOutput {
output: self.clone(),
}
}
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
serde_json::to_value(self).unwrap_or_else(|err| {
JsonValue::String(format!("failed to serialize mcp result: {err}"))
})
}
}
pub struct FunctionToolOutput {
@@ -272,12 +278,11 @@ fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue
}
},
ResponseInputItem::McpToolCallOutput { output, .. } => {
match output.as_function_call_output_payload().body {
FunctionCallOutputBody::Text(text) => JsonValue::String(text),
FunctionCallOutputBody::ContentItems(items) => {
content_items_to_code_mode_result(&items)
}
}
output.code_mode_result(&ToolPayload::Mcp {
server: String::new(),
tool: String::new(),
raw_arguments: String::new(),
})
}
}
}
@@ -413,6 +418,48 @@ mod tests {
}
}
#[test]
fn mcp_code_mode_result_serializes_full_call_tool_result() {
let output = CallToolResult {
content: vec![serde_json::json!({
"type": "text",
"text": "ignored",
})],
structured_content: Some(serde_json::json!({
"threadId": "thread_123",
"content": "done",
})),
is_error: Some(false),
meta: Some(serde_json::json!({
"source": "mcp",
})),
};
let result = output.code_mode_result(&ToolPayload::Mcp {
server: "server".to_string(),
tool: "tool".to_string(),
raw_arguments: "{}".to_string(),
});
assert_eq!(
result,
serde_json::json!({
"content": [{
"type": "text",
"text": "ignored",
}],
"structuredContent": {
"threadId": "thread_123",
"content": "done",
},
"isError": false,
"_meta": {
"source": "mcp",
},
})
);
}
#[test]
fn custom_tool_calls_can_derive_text_from_content_items() {
let payload = ToolPayload::Custom {