Plumb MCP turn metadata through _meta

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
nicholasclark-openai
2026-03-19 09:29:03 -07:00
parent 7df3c80f25
commit 9124fe9cbc
4 changed files with 86 additions and 10 deletions

View File

@@ -119,7 +119,8 @@ pub(crate) async fn handle_mcp_tool_call(
);
return CallToolResult::from_result(result);
}
let request_meta = build_mcp_tool_call_request_meta(&server, metadata.as_ref());
let request_meta =
build_mcp_tool_call_request_meta(turn_context.as_ref(), &server, metadata.as_ref());
let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
call_id: call_id.clone(),
@@ -390,18 +391,30 @@ pub(crate) struct McpToolApprovalMetadata {
const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps";
fn build_mcp_tool_call_request_meta(
turn_context: &TurnContext,
server: &str,
metadata: Option<&McpToolApprovalMetadata>,
) -> Option<serde_json::Value> {
if server != CODEX_APPS_MCP_SERVER_NAME {
return None;
let mut request_meta = serde_json::Map::new();
if let Some(turn_metadata) = turn_context.turn_metadata_state.current_meta_value() {
request_meta.insert(
crate::X_CODEX_TURN_METADATA_HEADER.to_string(),
turn_metadata,
);
}
let codex_apps_meta = metadata.and_then(|metadata| metadata.codex_apps_meta.as_ref())?;
if server == CODEX_APPS_MCP_SERVER_NAME
&& let Some(codex_apps_meta) =
metadata.and_then(|metadata| metadata.codex_apps_meta.clone())
{
request_meta.insert(
MCP_TOOL_CODEX_APPS_META_KEY.to_string(),
serde_json::Value::Object(codex_apps_meta),
);
}
Some(serde_json::json!({
MCP_TOOL_CODEX_APPS_META_KEY: codex_apps_meta,
}))
(!request_meta.is_empty()).then_some(serde_json::Value::Object(request_meta))
}
#[derive(Clone, Copy)]

View File

@@ -439,8 +439,28 @@ fn sanitize_mcp_tool_result_for_model_preserves_image_when_supported() {
assert_eq!(got, original);
}
#[test]
fn codex_apps_tool_call_request_meta_includes_codex_apps_meta() {
#[tokio::test]
async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
let (_session, turn_context) = make_session_and_context().await;
let meta =
build_mcp_tool_call_request_meta(&turn_context, "custom_server", /*metadata*/ None)
.expect("custom servers should receive turn metadata");
assert_eq!(
meta,
serde_json::json!({
crate::X_CODEX_TURN_METADATA_HEADER: {
"turn_id": turn_context.sub_id,
"sandbox": "workspace-write",
},
})
);
}
#[tokio::test]
async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps_meta() {
let (_session, turn_context) = make_session_and_context().await;
let metadata = McpToolApprovalMetadata {
annotations: None,
connector_id: Some("calendar".to_string()),
@@ -461,8 +481,16 @@ fn codex_apps_tool_call_request_meta_includes_codex_apps_meta() {
};
assert_eq!(
build_mcp_tool_call_request_meta(CODEX_APPS_MCP_SERVER_NAME, Some(&metadata)),
build_mcp_tool_call_request_meta(
&turn_context,
CODEX_APPS_MCP_SERVER_NAME,
Some(&metadata),
),
Some(serde_json::json!({
crate::X_CODEX_TURN_METADATA_HEADER: {
"turn_id": turn_context.sub_id,
"sandbox": "workspace-write",
},
MCP_TOOL_CODEX_APPS_META_KEY: {
"resource_uri": "connector://calendar/tools/calendar_create_event",
"contains_mcp_source": true,

View File

@@ -168,6 +168,11 @@ impl TurnMetadataState {
Some(self.base_header.clone())
}
pub(crate) fn current_meta_value(&self) -> Option<serde_json::Value> {
self.current_header_value()
.and_then(|header| serde_json::from_str(&header).ok())
}
pub(crate) fn spawn_git_enrichment_task(&self) {
if self.repo_root.is_none() {
return;

View File

@@ -424,6 +424,36 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
let requests = mock.requests();
assert_eq!(requests.len(), 3);
let apps_tool_call = server
.received_requests()
.await
.unwrap_or_default()
.into_iter()
.find_map(|request| {
let body: Value = serde_json::from_slice(&request.body)
.expect("apps request body should be valid json");
(request.url.path() == "/api/codex/apps"
&& body.get("method").and_then(Value::as_str) == Some("tools/call"))
.then_some(body)
})
.expect("apps tools/call request should be recorded");
assert_eq!(
apps_tool_call.pointer("/params/_meta/_codex_apps"),
Some(&json!({
"resource_uri": CALENDAR_CREATE_EVENT_RESOURCE_URI,
"contains_mcp_source": true,
"connector_id": "calendar",
}))
);
assert!(
apps_tool_call
.pointer("/params/_meta/x-codex-turn-metadata/turn_id")
.and_then(Value::as_str)
.is_some_and(|turn_id| !turn_id.is_empty()),
"apps tools/call should include turn metadata turn_id: {apps_tool_call:?}"
);
let first_request_tools = tool_names(&requests[0].body_json());
assert!(
first_request_tools