Codex MCP Call Rendering#
As of schema v22 and the current ingest pipeline, Decant parses MCP tool invocations from Codex event_msg records into first-class tool blocks with proper mcp__<server>__<base> naming, tool_kind = "mcp", and populated mcp_server fields. Earlier Codex rollouts logged native mcp_tool_call response items, while current rollouts emit only mcp_tool_call_end events — both formats are now fully supported.
On older Codex API logs, some MCP invocations were dispatched through a functions.exec orchestration wrapper. When that occurred, the nested mcp__<server>__<base> call (e.g., mcp__dosu__read_knowledge, mcp__dosu__whoami) was visible only inside the raw tool_input JSON and was not surfaced as a separate tool block. That exec-wrapper behavior is legacy; current Codex MCP calls are surfaced as distinct tool blocks.
How Codex Logs MCP Calls#
Codex session files consist of newline-delimited JSON records. Tool invocations appear in two places:
response_itemrecords whosepayload.typeis one offunction_call,custom_tool_call,tool_search_call, ormcp_tool_call. Nativemcp_tool_callitems carrynamespaceandnamefields that identify the MCP server and base tool name directly.event_msgrecords whosepayload.typeismcp_tool_call_end. Current Codex rollouts record MCP calls only as these end events; there is no matchingresponse_itemdurable copy and no begin event. Each event carries aninvocationobject (server, tool, arguments), acall_id, aduration, and aresult(Ok or Err).
Older logs sometimes routed MCP calls through a functions.exec orchestration wrapper, logging a function_call with name = "exec" and embedding the actual MCP invocation inside the arguments or input field. That pattern is legacy.
Decant's Ingest Behavior#
parseCodexSession in src/sources/codex.ts converts both response_item and event_msg records into flat NormalizedBlock structures:
MCP Event Parsing (Current)#
When the parser encounters an event_msg record whose payload.type is mcp_tool_call_end, it synthesizes a linked pair of messages:
- An assistant
tool_usemessage timestamped at the event timestamp minus the reportedduration, carrying themcp__<server>__<tool>name and the call arguments. - A tool
tool_resultmessage timestamped at the event itself, carrying the result text (extracted fromOk.contenttext entries, orOk.structuredContent, or the full serialized result forErrcases) and error state (isError = truewhenresult.Erris present orresult.Ok.isError === true).
The parser normalizes MCP names: if the server does not already start with mcp__, the prefix is added. Dotted and hyphenated tool names (e.g., slack.slack_read_thread) remain intact. Malformed events missing server, tool, or call_id are silently skipped as stream noise.
Response Item Parsing (Legacy and Fallback)#
Native mcp_tool_call response items follow the mcp__<server>__<base> convention and are classified as tool_kind = "mcp" with the server and base name split out .
Exec-wrapped MCP calls (legacy function_call records with name = "exec") are ingested with tool_name = "exec", tool_kind = "builtin", and mcp_server = NULL in the tool_call table. The nested mcp__<server>__<base> name and its arguments survive only in the raw input column.
Rendering in the UI#
classifyTool() in src/tools.ts inspects the tool name prefix: names starting with mcp__ become { kind: "mcp" }; everything else (including "exec") becomes { kind: "builtin" }. This classification drives how the transcript is displayed.
presentationForTool() in src/ui/transcript-rendering.ts renders MCP tools as highlighted JSON with source: "mcp".
Current behavior: MCP calls parsed from mcp_tool_call_end events are named mcp__<server>__<tool>, classified as tool_kind = "mcp", and rendered with the MCP server and tool name visible in the UI. Error state (both transport and tool errors) is surfaced in the transcript.
Legacy behavior: Exec-wrapped MCP calls are classified as "builtin" and fall through to the shell or generic JSON presentation path — the tool row label shows exec, not the MCP server or tool name. There is no Dosu-specific tool label, no mcp_server value, and no distinct UI affordance. The only visible trace of the nested invocation is the raw JSON in the tool input/output area.
Inspecting MCP Calls in a Session#
Current Codex logs: MCP calls appear as distinct tool blocks in the transcript with names like mcp__dosu__read_knowledge, mcp__codex_apps__slack.slack_read_thread, etc. The tool row label shows the server and tool name. Error state is visible inline.
To search programmatically, query the tool_call table filtering on tool_kind = 'mcp' or mcp_server IS NOT NULL. The tool_name, mcp_server, and tool_base_name columns are all populated for event-based MCP calls.
Legacy exec-wrapped calls: If you have older logs, look for tool blocks labeled exec. Expand the tool input — the nested mcp__<server>__<base> name and its arguments are embedded in the raw JSON payload. There is no separate MCP tool row for these. Query with tool_name = 'exec' and inspect the input column for mcp__ strings; mcp_server will be NULL.
Automatic Archive Enrichment#
Schema v22 introduced an ingest pipeline revision checkpoint. When you upgrade, existing ingest_source rows start at revision 0, so the next decant sync transactionally re-ingests each unchanged source file once, applying the new MCP event parser to your archive. Subsequent syncs skip the unchanged source again. Future parser improvements that need backfill will increment the pipeline revision; no manual archive deletion is required.