mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix(mcp): include threadId in both content and structuredContent in CallToolResult (#9338)
This commit is contained in:
committed by
GitHub
Unverified
parent
a6324ab34b
commit
99f47d6e9a
@@ -105,6 +105,24 @@ While a conversation runs, the server sends notifications:
|
||||
|
||||
Clients should render events and, when present, surface approval requests (see next section).
|
||||
|
||||
## Tool responses
|
||||
|
||||
The `codex` and `codex-reply` tools return standard MCP `CallToolResult` payloads. For
|
||||
compatibility with MCP clients that prefer `structuredContent`, Codex mirrors the
|
||||
content blocks inside `structuredContent` alongside the `threadId`.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{ "type": "text", "text": "Hello from Codex" }],
|
||||
"structuredContent": {
|
||||
"threadId": "019bbed6-1e9e-7f31-984c-a05b65045719",
|
||||
"content": "Hello from Codex"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Approvals (server → client)
|
||||
|
||||
When Codex needs approval to apply changes or run commands, the server issues JSON‑RPC requests to the client:
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_protocol::config_types::SandboxMode;
|
||||
use codex_utils_json_to_toml::json_to_toml;
|
||||
use mcp_types::Tool;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use mcp_types::ToolOutputSchema;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaSettings;
|
||||
use serde::Deserialize;
|
||||
@@ -127,8 +128,7 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
||||
name: "codex".to_string(),
|
||||
title: Some("Codex".to_string()),
|
||||
input_schema: tool_input_schema,
|
||||
// TODO(mbolin): This should be defined.
|
||||
output_schema: None,
|
||||
output_schema: Some(codex_tool_output_schema()),
|
||||
description: Some(
|
||||
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(),
|
||||
),
|
||||
@@ -136,6 +136,17 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_tool_output_schema() -> ToolOutputSchema {
|
||||
ToolOutputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"threadId": { "type": "string" },
|
||||
"content": { "type": "string" }
|
||||
})),
|
||||
required: Some(vec!["threadId".to_string(), "content".to_string()]),
|
||||
r#type: "object".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexToolCallParam {
|
||||
/// Returns the initial user prompt to start the Codex conversation and the
|
||||
/// effective Config object generated from the supplied parameters.
|
||||
@@ -239,7 +250,7 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
|
||||
name: "codex-reply".to_string(),
|
||||
title: Some("Codex Reply".to_string()),
|
||||
input_schema: tool_input_schema,
|
||||
output_schema: None,
|
||||
output_schema: Some(codex_tool_output_schema()),
|
||||
description: Some(
|
||||
"Continue a Codex conversation by providing the thread id and prompt.".to_string(),
|
||||
),
|
||||
@@ -330,6 +341,21 @@ mod tests {
|
||||
"type": "object"
|
||||
},
|
||||
"name": "codex",
|
||||
"outputSchema": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId",
|
||||
"content"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Codex"
|
||||
});
|
||||
assert_eq!(expected_tool_json, tool_json);
|
||||
@@ -362,6 +388,21 @@ mod tests {
|
||||
"type": "object",
|
||||
},
|
||||
"name": "codex-reply",
|
||||
"outputSchema": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId",
|
||||
"content"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Codex Reply",
|
||||
});
|
||||
assert_eq!(expected_tool_json, tool_json);
|
||||
|
||||
@@ -34,21 +34,27 @@ pub(crate) const INVALID_PARAMS_ERROR_CODE: i64 = -32602;
|
||||
|
||||
/// To adhere to MCP `tools/call` response format, include the Codex
|
||||
/// `threadId` in the `structured_content` field of the response.
|
||||
fn create_call_tool_result_with_thread_id(
|
||||
/// Some MCP clients ignore `content` when `structuredContent` is present, so
|
||||
/// mirror the text there as well.
|
||||
pub(crate) fn create_call_tool_result_with_thread_id(
|
||||
thread_id: ThreadId,
|
||||
text: String,
|
||||
is_error: Option<bool>,
|
||||
) -> CallToolResult {
|
||||
let content_text = text;
|
||||
let content = vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: content_text.clone(),
|
||||
annotations: None,
|
||||
})];
|
||||
let structured_content = json!({
|
||||
"threadId": thread_id,
|
||||
"content": content_text,
|
||||
});
|
||||
CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text,
|
||||
annotations: None,
|
||||
})],
|
||||
content,
|
||||
is_error,
|
||||
structured_content: Some(json!({
|
||||
"threadId": thread_id,
|
||||
})),
|
||||
structured_content: Some(structured_content),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +404,7 @@ mod tests {
|
||||
result.structured_content,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
"content": "done",
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -498,17 +498,11 @@ impl MessageProcessor {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
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 thread_id: {thread_id}"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: Some(json!({
|
||||
"threadId": thread_id,
|
||||
})),
|
||||
};
|
||||
let result = crate::codex_tool_runner::create_call_tool_result_with_thread_id(
|
||||
thread_id,
|
||||
format!("Session not found for thread_id: {thread_id}"),
|
||||
Some(true),
|
||||
);
|
||||
outgoing.send_response(request_id, result).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> {
|
||||
],
|
||||
"structuredContent": {
|
||||
"threadId": params.thread_id,
|
||||
"content": "File created!"
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -323,6 +324,7 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> {
|
||||
],
|
||||
"structuredContent": {
|
||||
"threadId": params.thread_id,
|
||||
"content": "Patch has been applied successfully!"
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -394,6 +396,7 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
|
||||
.and_then(|v| v.get("threadId"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("codex tool response should include structuredContent.threadId"),
|
||||
"content": "Enjoy!"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user