diff --git a/apps/backend/internal/agentctl/server/adapter/transport/codex/adapter.go b/apps/backend/internal/agentctl/server/adapter/transport/codex/adapter.go index 4973916b0b..e1548385b0 100644 --- a/apps/backend/internal/agentctl/server/adapter/transport/codex/adapter.go +++ b/apps/backend/internal/agentctl/server/adapter/transport/codex/adapter.go @@ -779,25 +779,25 @@ func (a *Adapter) handleNotification(method string, params json.RawMessage) { return } // Map Codex item types to tool call updates - // Item types: "userMessage", "agentMessage", "commandExecution", "fileChange", "reasoning" + // Item types: "userMessage", "agentMessage", "commandExecution", "fileChange", "reasoning", "mcpToolCall" switch p.Item.Type { - case "commandExecution": + case CodexItemCommandExecution: args := map[string]any{ "command": p.Item.Command, "cwd": p.Item.Cwd, } - normalizedPayload := a.normalizer.NormalizeToolCall("commandExecution", args) + normalizedPayload := a.normalizer.NormalizeToolCall(CodexItemCommandExecution, args) a.sendUpdate(AgentEvent{ Type: streams.EventTypeToolCall, SessionID: threadID, OperationID: turnID, ToolCallID: p.Item.ID, - ToolName: "commandExecution", + ToolName: CodexItemCommandExecution, ToolTitle: p.Item.Command, ToolStatus: "running", NormalizedPayload: normalizedPayload, }) - case "fileChange": + case CodexItemFileChange: // Build title from file paths var title string if len(p.Item.Changes) > 0 { @@ -817,13 +817,41 @@ func (a *Adapter) handleNotification(method string, params json.RawMessage) { args := map[string]any{ "changes": changesArgs, } - normalizedPayload := a.normalizer.NormalizeToolCall("fileChange", args) + normalizedPayload := a.normalizer.NormalizeToolCall(CodexItemFileChange, args) a.sendUpdate(AgentEvent{ Type: streams.EventTypeToolCall, SessionID: threadID, OperationID: turnID, ToolCallID: p.Item.ID, - ToolName: "fileChange", + ToolName: CodexItemFileChange, + ToolTitle: title, + ToolStatus: "running", + NormalizedPayload: normalizedPayload, + }) + case CodexItemMcpToolCall: + // Handle MCP tool calls (e.g., create_task_plan_kandev) + // Build title from server/tool + title := p.Item.Tool + if p.Item.Server != "" { + title = p.Item.Server + "/" + p.Item.Tool + } + // Parse arguments as map for normalization + var argsMap map[string]any + if len(p.Item.Arguments) > 0 { + _ = json.Unmarshal(p.Item.Arguments, &argsMap) + } + args := map[string]any{ + "server": p.Item.Server, + "tool": p.Item.Tool, + "arguments": argsMap, + } + normalizedPayload := a.normalizer.NormalizeToolCall(CodexItemMcpToolCall, args) + a.sendUpdate(AgentEvent{ + Type: streams.EventTypeToolCall, + SessionID: threadID, + OperationID: turnID, + ToolCallID: p.Item.ID, + ToolName: p.Item.Tool, // Use the actual MCP tool name for frontend display ToolTitle: title, ToolStatus: "running", NormalizedPayload: normalizedPayload, @@ -840,7 +868,7 @@ func (a *Adapter) handleNotification(method string, params json.RawMessage) { return } // Only send updates for tool-like items - if p.Item.Type == "commandExecution" || p.Item.Type == "fileChange" { + if p.Item.Type == CodexItemCommandExecution || p.Item.Type == CodexItemFileChange || p.Item.Type == CodexItemMcpToolCall { status := "complete" if p.Item.Status == "failed" { status = "error" @@ -856,13 +884,13 @@ func (a *Adapter) handleNotification(method string, params json.RawMessage) { // Include normalized payload for fallback message creation // This ensures the correct message type is used if the message doesn't exist yet switch p.Item.Type { - case "commandExecution": + case CodexItemCommandExecution: args := map[string]any{ "command": p.Item.Command, "cwd": p.Item.Cwd, } - update.NormalizedPayload = a.normalizer.NormalizeToolCall("commandExecution", args) - case "fileChange": + update.NormalizedPayload = a.normalizer.NormalizeToolCall(CodexItemCommandExecution, args) + case CodexItemFileChange: changesArgs := make([]any, 0, len(p.Item.Changes)) for _, c := range p.Item.Changes { changesArgs = append(changesArgs, map[string]any{ @@ -873,11 +901,32 @@ func (a *Adapter) handleNotification(method string, params json.RawMessage) { args := map[string]any{ "changes": changesArgs, } - update.NormalizedPayload = a.normalizer.NormalizeToolCall("fileChange", args) + update.NormalizedPayload = a.normalizer.NormalizeToolCall(CodexItemFileChange, args) + case CodexItemMcpToolCall: + // Parse arguments and result for normalization + var argsMap map[string]any + if len(p.Item.Arguments) > 0 { + _ = json.Unmarshal(p.Item.Arguments, &argsMap) + } + var resultMap any + if len(p.Item.Result) > 0 { + _ = json.Unmarshal(p.Item.Result, &resultMap) + } + args := map[string]any{ + "server": p.Item.Server, + "tool": p.Item.Tool, + "arguments": argsMap, + "result": resultMap, + } + if p.Item.ToolError != "" { + args["error"] = p.Item.ToolError + } + update.NormalizedPayload = a.normalizer.NormalizeToolCall(CodexItemMcpToolCall, args) + update.ToolName = p.Item.Tool // Use the actual MCP tool name } // Include diff for file changes - if p.Item.Type == "fileChange" && len(p.Item.Changes) > 0 { + if p.Item.Type == CodexItemFileChange && len(p.Item.Changes) > 0 { var diffs []string for _, c := range p.Item.Changes { if c.Diff != "" { diff --git a/apps/backend/internal/agentctl/server/adapter/transport/codex/normalize.go b/apps/backend/internal/agentctl/server/adapter/transport/codex/normalize.go index 95758da287..25f14a17f3 100644 --- a/apps/backend/internal/agentctl/server/adapter/transport/codex/normalize.go +++ b/apps/backend/internal/agentctl/server/adapter/transport/codex/normalize.go @@ -12,6 +12,7 @@ const ( CodexItemReasoning = "reasoning" CodexItemUserMessage = "userMessage" CodexItemAgentMessage = "agentMessage" + CodexItemMcpToolCall = "mcpToolCall" ) // Normalizer converts Codex protocol tool data to NormalizedPayload. @@ -31,6 +32,8 @@ func (n *Normalizer) NormalizeToolCall(toolName string, args map[string]any) *st return n.normalizeCommand(args) case CodexItemFileChange: return n.normalizeFileChange(args) + case CodexItemMcpToolCall: + return n.normalizeMcpToolCall(args) default: return n.normalizeGeneric(toolName, args) } @@ -109,6 +112,37 @@ func (n *Normalizer) normalizeFileChange(args map[string]any) *streams.Normalize return streams.NewModifyFile(filePath, mutations) } +// normalizeMcpToolCall converts Codex mcpToolCall item data. +// MCP tool calls have server, tool, arguments, result, and error fields. +func (n *Normalizer) normalizeMcpToolCall(args map[string]any) *streams.NormalizedPayload { + server := shared.GetString(args, "server") + tool := shared.GetString(args, "tool") + + // Create a generic payload with the MCP tool info + // The tool name is formatted as "server/tool" for display + toolName := tool + if server != "" { + toolName = server + "/" + tool + } + + // Include the arguments in the payload for display + displayArgs := map[string]any{ + "server": server, + "tool": tool, + } + if arguments, ok := args["arguments"]; ok { + displayArgs["arguments"] = arguments + } + if result, ok := args["result"]; ok { + displayArgs["result"] = result + } + if toolError, ok := args["error"]; ok { + displayArgs["error"] = toolError + } + + return streams.NewGeneric(toolName, displayArgs) +} + // normalizeGeneric wraps unknown items as generic. func (n *Normalizer) normalizeGeneric(toolName string, args map[string]any) *streams.NormalizedPayload { // Use factory function diff --git a/apps/backend/pkg/codex/types.go b/apps/backend/pkg/codex/types.go index 28559c6054..fe465a22cf 100644 --- a/apps/backend/pkg/codex/types.go +++ b/apps/backend/pkg/codex/types.go @@ -177,7 +177,7 @@ type TurnStartResult struct { // Item represents a Codex item (message, command, file change, etc.) type Item struct { ID string `json:"id"` - Type string `json:"type"` // "userMessage", "agentMessage", "commandExecution", "fileChange", "reasoning" + Type string `json:"type"` // "userMessage", "agentMessage", "commandExecution", "fileChange", "reasoning", "mcpToolCall" Status string `json:"status"` // "inProgress", "completed", "failed" // For commandExecution type @@ -194,6 +194,13 @@ type Item struct { // or plain strings. FlexibleContent handles both formats. Summary FlexibleContent `json:"summary,omitempty"` Content FlexibleContent `json:"content,omitempty"` + + // For mcpToolCall type + Server string `json:"server,omitempty"` + Tool string `json:"tool,omitempty"` + Arguments json.RawMessage `json:"arguments,omitempty"` + Result json.RawMessage `json:"result,omitempty"` + ToolError string `json:"error,omitempty"` // Named ToolError to avoid conflict with Error type } // ContentPart represents a content part in a Codex item.