Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions codex-rs/docs/codex_mcp_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
47 changes: 44 additions & 3 deletions codex-rs/mcp-server/src/codex_tool_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -127,15 +128,25 @@ 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(),
),
annotations: None,
}
}

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.
Expand Down Expand Up @@ -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(),
),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 16 additions & 9 deletions codex-rs/mcp-server/src/codex_tool_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -398,6 +404,7 @@ mod tests {
result.structured_content,
Some(json!({
"threadId": thread_id,
"content": "done",
}))
);
}
Expand Down
16 changes: 5 additions & 11 deletions codex-rs/mcp-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/mcp-server/tests/suite/codex_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> {
],
"structuredContent": {
"threadId": params.thread_id,
"content": "File created!"
}
}),
},
Expand Down Expand Up @@ -323,6 +324,7 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> {
],
"structuredContent": {
"threadId": params.thread_id,
"content": "Patch has been applied successfully!"
}
}),
},
Expand Down Expand Up @@ -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!"
}
})
);
Expand Down
Loading