feat(codex): add support for mcpToolCall item type (#159)

This commit is contained in:
João Salavisa
2026-02-02 23:17:28 +00:00
committed by GitHub
parent 0de3c31ddc
commit 4eaf467950
3 changed files with 104 additions and 14 deletions
@@ -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 != "" {
@@ -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
+8 -1
View File
@@ -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.