From 7bd1a38d53dbdb3519092132474e22247e039d44 Mon Sep 17 00:00:00 2001 From: Barry Roodt Date: Tue, 29 Sep 2026 15:57:39 +0200 Subject: [PATCH 1/2] fix(core): pass only MCP arguments as Codex tool-call input --- packages/core/src/agents/codex/parser.test.ts | 86 +++++++++++++++++++ packages/core/src/agents/codex/parser.ts | 20 +++-- packages/core/src/docs-results.test.ts | 40 --------- packages/core/src/docs-results.ts | 17 +--- 4 files changed, 102 insertions(+), 61 deletions(-) diff --git a/packages/core/src/agents/codex/parser.test.ts b/packages/core/src/agents/codex/parser.test.ts index 532e5f4e..b5056501 100644 --- a/packages/core/src/agents/codex/parser.test.ts +++ b/packages/core/src/agents/codex/parser.test.ts @@ -207,6 +207,92 @@ describe('codexParser', () => { }); }); + it("gives an mcp_tool_call's arguments as its input, keeping the result off it", () => { + // Shape from a real CLI run: the result can be far larger than the call. + const args = { project_id: 'p1', query: 'select 1' }; + const toolResult = { + content: [{ type: 'text', text: '[{"?column?":1}]' }], + structured_content: null, + }; + const stream = JSON.stringify({ + type: 'item.completed', + item: { + id: 'item_7', + type: 'mcp_tool_call', + server: 'supabase-mcp', + tool: 'execute_sql', + arguments: args, + result: toolResult, + error: null, + status: 'completed', + }, + }); + + const adapted = adaptTranscript(codexParser.parseTranscript(stream).events); + expect(adapted.transcript[0]).toMatchObject({ + name: 'execute_sql', + input: args, + output: toolResult, + }); + expect(adapted.toolCalls[0].body).toEqual(args); + expect(adapted.toolCalls[0].result).toEqual(toolResult); + }); + + it('gives an mcp_tool_call with no argument object an empty input', () => { + const stream = JSON.stringify({ + type: 'item.completed', + item: { + id: 'item_8', + type: 'mcp_tool_call', + server: 'supabase-mcp', + tool: 'list_tables', + arguments: null, + result: { content: [] }, + status: 'completed', + }, + }); + const adapted = adaptTranscript(codexParser.parseTranscript(stream).events); + expect(adapted.toolCalls[0].body).toEqual({}); + }); + + it.each([ + { + failure: 'an error the MCP tool returned', + result: { content: [{ type: 'text', text: 'column does not exist' }] }, + error: null, + }, + { + failure: 'an error Codex raised with no tool result', + result: null, + error: { message: 'column does not exist' }, + }, + ])( + 'surfaces $failure as the failed call error, not as input', + ({ result, error }) => { + const stream = JSON.stringify({ + type: 'item.completed', + item: { + id: 'item_9', + type: 'mcp_tool_call', + server: 'supabase-mcp', + tool: 'execute_sql', + arguments: { query: 'select child_table' }, + result, + error, + status: 'failed', + }, + }); + const adapted = adaptTranscript( + codexParser.parseTranscript(stream).events + ); + expect(adapted.toolCalls[0].body).toEqual({ + query: 'select child_table', + }); + expect(adapted.toolCalls[0].result).toBeUndefined(); + expect(adapted.toolCalls[0].error).toContain('column does not exist'); + } + ); + it("keeps a web_search item's action, which says what the hosted tool did", () => { const url = 'https://supabase.com/changelog.md'; const stream = JSON.stringify({ diff --git a/packages/core/src/agents/codex/parser.ts b/packages/core/src/agents/codex/parser.ts index f02afe10..8c0586a4 100644 --- a/packages/core/src/agents/codex/parser.ts +++ b/packages/core/src/agents/codex/parser.ts @@ -10,9 +10,12 @@ * * Observed `item.type`s: `agent_message` {text}, `reasoning` {text}, * `command_execution` {command, aggregated_output, exit_code, status}, - * `file_change` {changes:[{path,kind}], status}. MCP / web-search items are - * handled best-effort. Each tool item yields a paired tool_call + tool_result - * (correlated by the item id) so the adapter can attach the output. + * `file_change` {changes:[{path,kind}], status}, + * `mcp_tool_call` {server, tool, arguments, result, error, status}. Web-search + * items are handled best-effort. Each tool item yields a paired tool_call + + * tool_result (correlated by the item id) so the adapter can attach the output. + * A tool_call carries only the call's arguments; its output rides on the + * tool_result, the same split every other agent's parser makes. * * NB: this is the `--json` event schema, NOT the `~/.codex/sessions` rollout * format (event_msg/response_item) that older parsers targeted. @@ -178,13 +181,18 @@ function itemToEvents(item: Record): TranscriptEvent[] { // bare tool name; `item.server` names the MCP server when present. const bare = str(item.tool) ?? str(item.name) ?? 'mcp_tool_call'; const server = str(item.server); + const args = isRecord(item.arguments) ? item.arguments : {}; return toolCallPair( id, bare, - item, - item.result ?? item.output, + args, + // A failure Codex raised itself has no result, only `error: {message}`; + // carry that message as the output so the failure stays visible. + item.result ?? + item.output ?? + (isRecord(item.error) ? str(item.error.message) : undefined), statusSuccess(item.status), - {}, + extractArgs(args, CODEX_ARG_FIELDS), server ? { kind: 'mcp', server, toolName: bare } : { kind: 'other', toolName: bare } diff --git a/packages/core/src/docs-results.test.ts b/packages/core/src/docs-results.test.ts index f21adf42..dabb0102 100644 --- a/packages/core/src/docs-results.test.ts +++ b/packages/core/src/docs-results.test.ts @@ -99,46 +99,6 @@ describe('buildDocsResult', () => { expect(result.calls[0].hasContent).toBe(false); }); - it('finds the query nested under body.arguments (real Codex mcp_tool_call shape)', () => { - const result = buildDocsResult([ - toolCall( - // Codex's parser sets originalName to the MCP tool's own name - // (item.tool), not the "mcp_tool_call" item type, and reports the - // whole raw item as `body`, args nested under `body.arguments`. - 'search_docs', - { - id: 'item_9', - type: 'mcp_tool_call', - server: 'supabase-mcp', - tool: 'search_docs', - arguments: { - graphql_query: - '{ searchDocs(query: "rls") { nodes { title href } } }', - }, - }, - { - result: { - searchDocs: { - nodes: [ - { - title: 'Row Level Security', - href: 'https://supabase.com/docs/guides/database/postgres/row-level-security', - }, - ], - }, - }, - } - ), - ]); - - expect(result.calls[0].query).toBe( - '{ searchDocs(query: "rls") { nodes { title href } } }' - ); - expect(result.calls[0].pages.map((p) => p.url)).toEqual([ - 'https://supabase.com/docs/guides/database/postgres/row-level-security', - ]); - }); - it('unwraps a content-array result whose text field is a JSON-encoded string (real Claude Code shape)', () => { const result = buildDocsResult([ toolCall( diff --git a/packages/core/src/docs-results.ts b/packages/core/src/docs-results.ts index 95894c06..dc9b520d 100644 --- a/packages/core/src/docs-results.ts +++ b/packages/core/src/docs-results.ts @@ -51,19 +51,6 @@ const STDOUT_REDIRECT_PATTERN = /(?:^|[^2])>/; // legally follow one (`|`, `>`, quotes, backslash-escapes). const URL_IN_COMMAND_PATTERN = /https?:\/\/[^\s'"`\\;|&>()]+/g; -/** The search_docs query arg, flat on `body` or nested under `body.arguments` (Codex's shape). */ -function extractGraphqlQuery( - body: Record -): string | undefined { - if (typeof body.graphql_query === 'string') return body.graphql_query; - const args = body.arguments; - if (args && typeof args === 'object' && 'graphql_query' in args) { - const value = (args as Record).graphql_query; - if (typeof value === 'string') return value; - } - return undefined; -} - /** * True for the apex supabase.com host. Docs, changelog, and blog all live * there. Subdomains like `api.` and `mcp.` are service endpoints, so they @@ -242,8 +229,8 @@ export function buildDocsResult(toolCalls: ToolCallRecord[]): DocsResult { const { tool, body, result } = call; if (tool.toolName === 'search_docs') { - const graphqlQuery = extractGraphqlQuery(body); - if (!graphqlQuery) continue; + const graphqlQuery = body.graphql_query; + if (typeof graphqlQuery !== 'string' || !graphqlQuery) continue; calls.push({ source: 'search_docs', query: graphqlQuery, From 6873f96f796e193c91b9193091764e09cc73fd99 Mon Sep 17 00:00:00 2001 From: Barry Roodt Date: Mon, 5 Oct 2026 12:01:20 +0200 Subject: [PATCH 2/2] fix(core): keep Codex MCP normalized args empty --- packages/core/src/agents/codex/parser.test.ts | 4 +++- packages/core/src/agents/codex/parser.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agents/codex/parser.test.ts b/packages/core/src/agents/codex/parser.test.ts index b5056501..577fe0c0 100644 --- a/packages/core/src/agents/codex/parser.test.ts +++ b/packages/core/src/agents/codex/parser.test.ts @@ -229,10 +229,12 @@ describe('codexParser', () => { }); const adapted = adaptTranscript(codexParser.parseTranscript(stream).events); - expect(adapted.transcript[0]).toMatchObject({ + expect(adapted.transcript[0]).toEqual({ + type: 'tool_call', name: 'execute_sql', input: args, output: toolResult, + error: undefined, }); expect(adapted.toolCalls[0].body).toEqual(args); expect(adapted.toolCalls[0].result).toEqual(toolResult); diff --git a/packages/core/src/agents/codex/parser.ts b/packages/core/src/agents/codex/parser.ts index 8c0586a4..8a14403d 100644 --- a/packages/core/src/agents/codex/parser.ts +++ b/packages/core/src/agents/codex/parser.ts @@ -192,7 +192,7 @@ function itemToEvents(item: Record): TranscriptEvent[] { item.output ?? (isRecord(item.error) ? str(item.error.message) : undefined), statusSuccess(item.status), - extractArgs(args, CODEX_ARG_FIELDS), + {}, server ? { kind: 'mcp', server, toolName: bare } : { kind: 'other', toolName: bare }