Propagate thread id in MCP tool metadata (#18093)

## Summary
- attach the authoritative Codex thread id to MCP tool request
`_meta.threadId` for model-initiated tool calls
- attach the same thread id for manual `mcpServer/tool/call` requests
before invoking the MCP server
- cover both metadata helper behavior and the manual app-server MCP path
in tests


needed because the Rust app-server is the last place that still has
authoritative knowledge of “this model-generated MCP tool call belongs
to conversation/thread X” before the request leaves Codex and reaches
Hoopa. It adds threadId to MCP request metadata in the model-generated
tool-call path, using sess.conversation_id, and also does the same for
the manual mcpServer/tool/call path.

## Test plan
- `cargo test -p codex-core
mcp_tool_call_thread_id_meta_is_added_to_request_meta --lib`
- `cargo test -p codex-app-server
mcp_server_tool_call_returns_tool_result`

Paired Hoopa consumer PR: https://github.com/openai/openai/pull/833263
This commit is contained in:
Rennie
2026-04-21 10:09:46 -07:00
committed by GitHub
parent 48f82ca7c5
commit 3a9df58d06
4 changed files with 97 additions and 4 deletions
+27
View File
@@ -477,6 +477,8 @@ async fn execute_mcp_tool_call(
metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()),
)
.await?;
let request_meta =
with_mcp_tool_call_thread_id_meta(request_meta, &sess.conversation_id.to_string());
let request_meta =
augment_mcp_tool_request_meta_with_sandbox_state(sess, turn_context, server, request_meta)
.await
@@ -660,6 +662,7 @@ pub(crate) struct McpToolApprovalMetadata {
const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps";
const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
fn custom_mcp_tool_approval_mode(
turn_context: &TurnContext,
@@ -714,6 +717,30 @@ fn build_mcp_tool_call_request_meta(
(!request_meta.is_empty()).then_some(serde_json::Value::Object(request_meta))
}
fn with_mcp_tool_call_thread_id_meta(
meta: Option<serde_json::Value>,
thread_id: &str,
) -> Option<serde_json::Value> {
match meta {
Some(serde_json::Value::Object(mut map)) => {
map.insert(
MCP_TOOL_THREAD_ID_META_KEY.to_string(),
serde_json::Value::String(thread_id.to_string()),
);
Some(serde_json::Value::Object(map))
}
None => {
let mut map = serde_json::Map::new();
map.insert(
MCP_TOOL_THREAD_ID_META_KEY.to_string(),
serde_json::Value::String(thread_id.to_string()),
);
Some(serde_json::Value::Object(map))
}
other => other,
}
}
#[derive(Clone, Copy)]
struct McpToolApprovalPromptOptions {
allow_session_remember: bool,
+29
View File
@@ -651,6 +651,35 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
);
}
#[test]
fn mcp_tool_call_thread_id_meta_is_added_to_request_meta() {
assert_eq!(
with_mcp_tool_call_thread_id_meta(
Some(serde_json::json!({
"source": "test-client",
"threadId": "stale-thread",
})),
"thread-live",
),
Some(serde_json::json!({
"source": "test-client",
"threadId": "thread-live",
}))
);
assert_eq!(
with_mcp_tool_call_thread_id_meta(/*meta*/ None, "thread-live"),
Some(serde_json::json!({
"threadId": "thread-live",
}))
);
assert_eq!(
with_mcp_tool_call_thread_id_meta(Some(serde_json::json!("invalid-meta")), "thread-live"),
Some(serde_json::json!("invalid-meta"))
);
}
#[test]
fn accepted_elicitation_content_converts_to_request_user_input_response() {
let response = request_user_input_response_from_elicitation_content(Some(serde_json::json!(