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
32 changes: 25 additions & 7 deletions src-tauri/src/parser/toolcall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct PendingCall {
pub input_text: Option<String>,
/// Raw namespace from the function_call payload (e.g. "mcp__codex_apps__github").
pub namespace: Option<String>,
/// v0.130.0+: direct MCP server name from tool_id.server (bypasses namespace parsing).
pub mcp_server: Option<String>,
}

/// Builder that collects function_call / custom_tool_call entries and finalizes
Expand Down Expand Up @@ -77,6 +79,7 @@ impl ToolCallBuilder {
name: String,
arguments_str: &str,
namespace: Option<String>,
mcp_server_direct: Option<String>,
) {
let arguments = serde_json::from_str(arguments_str).unwrap_or(Value::Null);
self.pending.insert(
Expand All @@ -86,6 +89,7 @@ impl ToolCallBuilder {
arguments,
input_text: None,
namespace,
mcp_server: mcp_server_direct,
},
);
}
Expand All @@ -99,6 +103,7 @@ impl ToolCallBuilder {
arguments: Value::Object(serde_json::Map::new()),
input_text: input,
namespace: None,
mcp_server: None,
},
);
}
Expand Down Expand Up @@ -225,14 +230,23 @@ impl ToolCallBuilder {
return;
}

let (kind, mcp_server, mcp_tool) = match &pending.namespace {
Some(ns) if ns.starts_with("mcp__") => {
let (server, tool) = parse_mcp_namespace(ns, &pending.name);
(ToolKind::McpTool, server, tool)
// v0.130.0+: direct mcp_server from tool_id takes precedence over namespace parsing.
let (kind, mcp_server, mcp_tool) = if let Some(ref server) = pending.mcp_server {
(
ToolKind::McpTool,
Some(server.clone()),
Some(pending.name.clone()),
)
} else {
match &pending.namespace {
Some(ns) if ns.starts_with("mcp__") => {
let (server, tool) = parse_mcp_namespace(ns, &pending.name);
(ToolKind::McpTool, server, tool)
}
_ if pending.name == "wait_agent" => (ToolKind::WaitAgent, None, None),
_ if pending.name == "close_agent" => (ToolKind::CloseAgent, None, None),
_ => (ToolKind::Unknown, None, None),
}
_ if pending.name == "wait_agent" => (ToolKind::WaitAgent, None, None),
_ if pending.name == "close_agent" => (ToolKind::CloseAgent, None, None),
_ => (ToolKind::Unknown, None, None),
};
self.finalized.push(ToolCall {
call_id: call_id.to_string(),
Expand Down Expand Up @@ -299,6 +313,7 @@ impl ToolCallBuilder {
arguments: Value::Null,
input_text: None,
namespace: None,
mcp_server: None,
});

let command: Option<Vec<String>> = payload
Expand Down Expand Up @@ -359,6 +374,7 @@ impl ToolCallBuilder {
arguments: Value::Null,
input_text: None,
namespace: None,
mcp_server: None,
});

// Extract server + tool from invocation field, then namespace, then name.
Expand Down Expand Up @@ -462,6 +478,7 @@ impl ToolCallBuilder {
arguments: Value::Null,
input_text: None,
namespace: None,
mcp_server: None,
});

self.finalized.push(ToolCall {
Expand Down Expand Up @@ -619,6 +636,7 @@ impl ToolCallBuilder {
arguments: Value::Null,
input_text: None,
namespace: None,
mcp_server: None,
});
let output = ["output", "aggregated_output", "stdout"]
.iter()
Expand Down
85 changes: 84 additions & 1 deletion src-tauri/src/parser/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,20 @@ fn handle_response_item(
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
builder.add_function_call(call_id, name, arguments_str, namespace);
// v0.130.0+ (PR #21454): string-keyed MCP tool maps removed; function_call
// entries now carry tool_id: { server, tool } instead of a flat namespace string.
// Store the server directly to avoid parse_mcp_namespace misinterpreting it.
let mcp_server_direct = if namespace.is_none() {
payload
.get("tool_id")
.and_then(|tid| tid.get("server"))
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
} else {
None
};
builder.add_function_call(call_id, name, arguments_str, namespace, mcp_server_direct);
}

"function_call_output" => {
Expand Down Expand Up @@ -1274,4 +1287,74 @@ mod tests {
"turn-3 has_compaction set from compacted entry"
);
}

// Codex v0.130.0 (PR #21454): string-keyed MCP tool maps removed.
// function_call entries for MCP tools now carry tool_id: { server, tool }
// instead of a flat namespace string. Verify the tool is still classified as McpTool.
#[test]
fn function_call_with_tool_id_classified_as_mcp_tool() {
let entries = entries(&[
r#"{"timestamp":"2026-05-08T10:00:00Z","type":"session_meta","payload":{"id":"s-v130","timestamp":"2026-05-08T10:00:00Z","cli_version":"0.130.0"}}"#,
r#"{"timestamp":"2026-05-08T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#,
r#"{"timestamp":"2026-05-08T10:00:02Z","type":"response_item","payload":{"type":"function_call","call_id":"mcp-tc1","name":"get_pr_info","tool_id":{"server":"github","tool":"get_pr_info"},"arguments":"{\"pr_number\":42}"}}"#,
r#"{"timestamp":"2026-05-08T10:00:03Z","type":"response_item","payload":{"type":"function_call_output","call_id":"mcp-tc1","output":"PR #42: Fix the bug"}}"#,
r#"{"timestamp":"2026-05-08T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1746698404.0}}"#,
]);

let turns = build_turns(&entries);

assert_eq!(turns.len(), 1);
assert_eq!(turns[0].tool_calls.len(), 1);
let tool = &turns[0].tool_calls[0];
assert_eq!(tool.kind, ToolKind::McpTool);
assert_eq!(tool.call_id, "mcp-tc1");
assert_eq!(tool.name, "get_pr_info");
assert_eq!(tool.mcp_server.as_deref(), Some("github"));
assert_eq!(tool.mcp_tool.as_deref(), Some("get_pr_info"));
assert_eq!(tool.output.as_deref(), Some("PR #42: Fix the bug"));
assert_eq!(tool.status, "completed");
}

#[test]
fn function_call_with_tool_id_multi_segment_server() {
// tool_id.server may contain __ separators (e.g. "codex_apps__slack")
let entries = entries(&[
r#"{"timestamp":"2026-05-08T10:00:00Z","type":"session_meta","payload":{"id":"s-v130b","timestamp":"2026-05-08T10:00:00Z"}}"#,
r#"{"timestamp":"2026-05-08T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#,
r#"{"timestamp":"2026-05-08T10:00:02Z","type":"response_item","payload":{"type":"function_call","call_id":"mcp-tc2","name":"post_message","tool_id":{"server":"codex_apps__slack","tool":"post_message"},"arguments":"{\"channel\":\"general\",\"text\":\"hello\"}"}}"#,
r#"{"timestamp":"2026-05-08T10:00:03Z","type":"response_item","payload":{"type":"function_call_output","call_id":"mcp-tc2","output":"ok"}}"#,
r#"{"timestamp":"2026-05-08T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1746698404.0}}"#,
]);

let turns = build_turns(&entries);

assert_eq!(turns.len(), 1);
assert_eq!(turns[0].tool_calls.len(), 1);
let tool = &turns[0].tool_calls[0];
assert_eq!(tool.kind, ToolKind::McpTool);
assert_eq!(tool.mcp_server.as_deref(), Some("codex_apps__slack"));
assert_eq!(tool.mcp_tool.as_deref(), Some("post_message"));
}

#[test]
fn function_call_namespace_still_works_without_tool_id() {
// Pre-v0.130.0 sessions with namespace field must continue to work unchanged.
let entries = entries(&[
r#"{"timestamp":"2026-05-08T10:00:00Z","type":"session_meta","payload":{"id":"s-pre130","timestamp":"2026-05-08T10:00:00Z"}}"#,
r#"{"timestamp":"2026-05-08T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#,
r#"{"timestamp":"2026-05-08T10:00:02Z","type":"response_item","payload":{"type":"function_call","call_id":"mcp-old1","name":"_get_pr_info","namespace":"mcp__codex_apps__github","arguments":"{\"pr_number\":7}"}}"#,
r#"{"timestamp":"2026-05-08T10:00:03Z","type":"response_item","payload":{"type":"function_call_output","call_id":"mcp-old1","output":"PR #7"}}"#,
r#"{"timestamp":"2026-05-08T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1746698404.0}}"#,
]);

let turns = build_turns(&entries);

assert_eq!(turns.len(), 1);
assert_eq!(turns[0].tool_calls.len(), 1);
let tool = &turns[0].tool_calls[0];
assert_eq!(tool.kind, ToolKind::McpTool);
assert_eq!(tool.mcp_server.as_deref(), Some("codex_apps__github"));
assert_eq!(tool.mcp_tool.as_deref(), Some("github_get_pr_info"));
assert_eq!(tool.output.as_deref(), Some("PR #7"));
}
}