Preserve encrypted content in MCP tool outputs (#33509)

## What changed

- Convert MCP text content marked with `_meta["codex/encryptedContent"] = true`
  into `encrypted_content` function call output items.
- Prefer the content-item payload whenever encrypted content is present so that
  `structured_content` does not replace the encrypted output.

## Testing

- Add a stdio MCP integration test that round-trips mixed plaintext and
  encrypted tool output.

GitOrigin-RevId: c3997a16b06ad38ff22b21016ede35b97974d9ec
This commit is contained in:
rka-oai
2026-07-16 05:38:36 +00:00
committed by copyberry
parent 1d941253e9
commit cbc83d961e
3 changed files with 157 additions and 7 deletions
+90
View File
@@ -1272,6 +1272,96 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
#[serial(mcp_test_value)]
async fn stdio_encrypted_content_responses_round_trip() -> anyhow::Result<()> {
skip_if_wine_exec!(
Ok(()),
"requires a Windows test_stdio_server in the Wine-exec environment"
);
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let call_id = "encrypted-1";
let server_name = "rmcp";
let namespace = format!("mcp__{server_name}");
mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call_with_namespace(
call_id,
&namespace,
"encrypted_output",
"{}",
),
responses::ev_completed("resp-1"),
]),
)
.await;
let final_mock = mount_sse_once(
&server,
responses::sse(vec![
responses::ev_assistant_message("msg-1", "rmcp tool completed successfully."),
responses::ev_completed("resp-2"),
]),
)
.await;
let rmcp_test_server_bin = remote_aware_stdio_server_bin()?;
let fixture = test_codex()
.with_config(move |config| {
insert_mcp_server(
config,
server_name,
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
TestMcpServerOptions {
environment_id: remote_aware_environment_id(),
..Default::default()
},
);
})
.build_with_auto_env(&server)
.await?;
wait_for_mcp_server(&fixture.codex, server_name).await?;
fixture
.codex
.submit(read_only_user_turn(
&fixture,
"call the rmcp encrypted output tool",
))
.await?;
wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let output_item = final_mock.single_request().function_call_output(call_id);
let output = output_item["output"]
.as_array()
.expect("encrypted MCP output should be content items");
assert_eq!(output.len(), 3);
assert_wall_time_header(
output[0]["text"]
.as_str()
.expect("first encrypted MCP output item should be wall-time text"),
);
assert_eq!(
&output[1..],
&[
json!({
"type": "input_text",
"text": "Lookup completed",
}),
json!({
"type": "encrypted_content",
"encrypted_content": "gAAAA-test",
}),
]
);
server.verify().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
#[serial(mcp_test_value)]
async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {