feat: add threadId to MCP server messages (#9192)

This favors `threadId` instead of `conversationId` so we use the same
terms as https://developers.openai.com/codex/sdk/.

To test the local build:

```
cd codex-rs
cargo build --bin codex
npx -y @modelcontextprotocol/inspector ./target/debug/codex mcp-server
```

I sent:

```json
{
  "method": "tools/call",
  "params": {
    "name": "codex",
    "arguments": {
      "prompt": "favorite ls option?"
    },
    "_meta": {
      "progressToken": 0
    }
  }
}
```

and got:

```json
{
  "content": [
    {
      "type": "text",
      "text": "`ls -lah` (or `ls -alh`) — long listing, includes dotfiles, human-readable sizes."
    }
  ],
  "structuredContent": {
    "threadId": "019bbb20-bff6-7130-83aa-bf45ab33250e"
  }
}
```

and successfully used the `threadId` in the follow-up with the
`codex-reply` tool call:

```json
{
  "method": "tools/call",
  "params": {
    "name": "codex-reply",
    "arguments": {
      "prompt": "what is the long versoin",
      "threadId": "019bbb20-bff6-7130-83aa-bf45ab33250e"
    },
    "_meta": {
      "progressToken": 1
    }
  }
}
```

whose response also has the `threadId`:

```json
{
  "content": [
    {
      "type": "text",
      "text": "Long listing is `ls -l` (adds permissions, owner/group, size, timestamp)."
    }
  ],
  "structuredContent": {
    "threadId": "019bbb20-bff6-7130-83aa-bf45ab33250e"
  }
}
```

Fixes https://github.com/openai/codex/issues/3712.
This commit is contained in:
Michael Bolin
2026-01-13 22:14:41 -08:00
committed by GitHub
parent 5675af5190
commit 0c09dc3c03
7 changed files with 270 additions and 90 deletions

View File

@@ -433,10 +433,7 @@ impl MessageProcessor {
tracing::info!("tools/call -> params: {:?}", arguments);
// parse arguments
let CodexToolCallReplyParam {
conversation_id,
prompt,
} = match arguments {
let codex_tool_call_reply_param: CodexToolCallReplyParam = match arguments {
Some(json_val) => match serde_json::from_value::<CodexToolCallReplyParam>(json_val) {
Ok(params) => params,
Err(e) => {
@@ -457,12 +454,12 @@ impl MessageProcessor {
},
None => {
tracing::error!(
"Missing arguments for codex-reply tool-call; the `conversation_id` and `prompt` fields are required."
"Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required."
);
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_owned(),
text: "Missing arguments for codex-reply tool-call; the `conversation_id` and `prompt` fields are required.".to_owned(),
text: "Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.".to_owned(),
annotations: None,
})],
is_error: Some(true),
@@ -473,14 +470,15 @@ impl MessageProcessor {
return;
}
};
let conversation_id = match ThreadId::from_string(&conversation_id) {
let thread_id = match codex_tool_call_reply_param.get_thread_id() {
Ok(id) => id,
Err(e) => {
tracing::error!("Failed to parse conversation_id: {e}");
tracing::error!("Failed to parse thread_id: {e}");
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_owned(),
text: format!("Failed to parse conversation_id: {e}"),
text: format!("Failed to parse thread_id: {e}"),
annotations: None,
})],
is_error: Some(true),
@@ -496,18 +494,20 @@ impl MessageProcessor {
let outgoing = self.outgoing.clone();
let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone();
let codex = match self.thread_manager.get_thread(conversation_id).await {
let codex = match self.thread_manager.get_thread(thread_id).await {
Ok(c) => c,
Err(_) => {
tracing::warn!("Session not found for conversation_id: {conversation_id}");
tracing::warn!("Session not found for thread_id: {thread_id}");
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_owned(),
text: format!("Session not found for conversation_id: {conversation_id}"),
text: format!("Session not found for thread_id: {thread_id}"),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
structured_content: Some(json!({
"threadId": thread_id,
})),
};
outgoing.send_response(request_id, result).await;
return;
@@ -515,19 +515,19 @@ impl MessageProcessor {
};
// Spawn the long-running reply handler.
let prompt = codex_tool_call_reply_param.prompt.clone();
tokio::spawn({
let outgoing = outgoing.clone();
let prompt = prompt.clone();
let running_requests_id_to_codex_uuid = running_requests_id_to_codex_uuid.clone();
async move {
crate::codex_tool_runner::run_codex_tool_session_reply(
thread_id,
codex,
outgoing,
request_id,
prompt,
running_requests_id_to_codex_uuid,
conversation_id,
)
.await;
}
@@ -563,8 +563,8 @@ impl MessageProcessor {
RequestId::Integer(i) => i.to_string(),
};
// Obtain the conversation id while holding the first lock, then release.
let conversation_id = {
// Obtain the thread id while holding the first lock, then release.
let thread_id = {
let map_guard = self.running_requests_id_to_codex_uuid.lock().await;
match map_guard.get(&request_id) {
Some(id) => *id,
@@ -574,13 +574,13 @@ impl MessageProcessor {
}
}
};
tracing::info!("conversation_id: {conversation_id}");
tracing::info!("thread_id: {thread_id}");
// Obtain the Codex conversation from the server.
let codex_arc = match self.thread_manager.get_thread(conversation_id).await {
// Obtain the Codex thread from the server.
let codex_arc = match self.thread_manager.get_thread(thread_id).await {
Ok(c) => c,
Err(_) => {
tracing::warn!("Session not found for conversation_id: {conversation_id}");
tracing::warn!("Session not found for thread_id: {thread_id}");
return;
}
};