Skip to content

Commit d2b90ed

Browse files
LLM-26222 [Codex] Show input params, result of running mcp tools
1 parent 3a71ad9 commit d2b90ed

8 files changed

Lines changed: 290 additions & 6 deletions

src/CodexAcpServer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface SessionState {
4141
account: Account | null;
4242
cwd: string;
4343
sessionMcpServers?: Array<string>;
44+
mcpToolLogs: Map<string, Array<string>>;
4445
}
4546

4647
export class CodexAcpServer implements acp.Agent {
@@ -156,6 +157,7 @@ export class CodexAcpServer implements acp.Agent {
156157
account: accountResponse.account,
157158
cwd: request.cwd,
158159
sessionMcpServers: sessionMcpServers,
160+
mcpToolLogs: new Map(),
159161
}
160162
this.sessions.set(sessionId, sessionState);
161163

@@ -356,6 +358,7 @@ export class CodexAcpServer implements acp.Agent {
356358
account: accountResponse.account,
357359
cwd: request.cwd,
358360
sessionMcpServers: sessionMcpServers,
361+
mcpToolLogs: new Map(),
359362
};
360363
this.sessions.set(sessionId, sessionState);
361364

src/CodexEventHandler.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
createCommandExecutionUpdate,
2626
createDynamicToolCallUpdate,
2727
createFileChangeUpdate,
28+
createMcpRawInput,
29+
createMcpRawOutput,
2830
createFuzzyFileSearchComplete,
2931
createFuzzyFileSearchStartOrUpdate,
3032
createMcpToolCallUpdate,
@@ -101,12 +103,13 @@ export class CodexEventHandler {
101103
case "turn/diff/updated":
102104
case "item/commandExecution/terminalInteraction":
103105
case "item/fileChange/outputDelta":
104-
case "item/mcpToolCall/progress":
105106
case "serverRequest/resolved":
106107
case "account/updated":
107108
case "fs/changed":
108109
case "mcpServer/startupStatus/updated":
109110
return null;
111+
case "item/mcpToolCall/progress":
112+
return this.createMcpToolProgressEvent(notification.params);
110113
case "account/rateLimits/updated":
111114
this.handleRateLimitsUpdated(notification.params);
112115
return null;
@@ -213,10 +216,19 @@ export class CodexEventHandler {
213216
case "mcpToolCall":
214217
case "fileChange":
215218
case "dynamicToolCall":
219+
const mcpLogs = event.item.type === "mcpToolCall"
220+
? this.consumeMcpToolLogs(event.item.id)
221+
: [];
216222
return {
217223
sessionUpdate: "tool_call_update",
218224
toolCallId: event.item.id,
219-
status: event.item.status === "completed" ? "completed" : "failed"
225+
status: event.item.status === "completed" ? "completed" : "failed",
226+
rawInput: event.item.type === "mcpToolCall"
227+
? createMcpRawInput(event.item.server, event.item.tool, event.item.arguments)
228+
: undefined,
229+
rawOutput: event.item.type === "mcpToolCall"
230+
? createMcpRawOutput(mcpLogs, event.item.result, event.item.error)
231+
: undefined,
220232
}
221233
case "commandExecution":
222234
return this.completeCommandExecutionEvent(event.item);
@@ -258,6 +270,37 @@ export class CodexEventHandler {
258270
}
259271
}
260272

273+
private createMcpToolProgressEvent(event: { itemId: string, message: string }): UpdateSessionEvent {
274+
const logs = this.appendMcpToolLog(event.itemId, event.message);
275+
return {
276+
sessionUpdate: "tool_call_update",
277+
toolCallId: event.itemId,
278+
rawOutput: {
279+
formatted_output: logs.join("\n\n"),
280+
}
281+
};
282+
}
283+
284+
private appendMcpToolLog(toolCallId: string, message: string): Array<string> {
285+
const cleaned = message.trim();
286+
if (cleaned.length === 0) {
287+
return this.sessionState.mcpToolLogs.get(toolCallId) ?? [];
288+
}
289+
290+
const logs = this.sessionState.mcpToolLogs.get(toolCallId) ?? [];
291+
if (logs.at(-1) !== cleaned && !logs.includes(cleaned)) {
292+
logs.push(cleaned);
293+
}
294+
this.sessionState.mcpToolLogs.set(toolCallId, logs);
295+
return logs;
296+
}
297+
298+
private consumeMcpToolLogs(toolCallId: string): Array<string> {
299+
const logs = this.sessionState.mcpToolLogs.get(toolCallId) ?? [];
300+
this.sessionState.mcpToolLogs.delete(toolCallId);
301+
return logs;
302+
}
303+
261304
private completeCommandExecutionEvent(item: ThreadItem & { "type": "commandExecution" }): UpdateSessionEvent {
262305
return {
263306
sessionUpdate: "tool_call_update",

src/CodexToolCallMapper.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type {
1313
CommandExecutionStatus,
1414
DynamicToolCallStatus,
1515
FileUpdateChange,
16+
McpToolCallError,
17+
McpToolCallResult,
1618
McpToolCallStatus,
1719
PatchApplyStatus,
1820
ThreadItem,
@@ -84,7 +86,12 @@ export async function createCommandExecutionUpdate(
8486
export async function createMcpToolCallUpdate(
8587
item: ThreadItem & { type: "mcpToolCall" }
8688
): Promise<UpdateSessionEvent> {
87-
return createExecuteToolCallUpdate(item, `mcp.${item.server}.${item.tool}`);
89+
return createExecuteToolCallUpdate(
90+
item,
91+
`mcp.${item.server}.${item.tool}`,
92+
createMcpRawInput(item.server, item.tool, item.arguments),
93+
createMcpRawOutput([], item.result, item.error),
94+
);
8895
}
8996

9097
export async function createDynamicToolCallUpdate(
@@ -96,7 +103,8 @@ export async function createDynamicToolCallUpdate(
96103
export async function createExecuteToolCallUpdate(
97104
item: ThreadItem & ({ type: "mcpToolCall" } | { type: "dynamicToolCall" }),
98105
title: string,
99-
rawInput?: { arguments: JsonValue }
106+
rawInput?: Record<string, JsonValue | string>,
107+
rawOutput?: Record<string, JsonValue | string | null>,
100108
): Promise<UpdateSessionEvent> {
101109
return {
102110
sessionUpdate: "tool_call",
@@ -105,9 +113,88 @@ export async function createExecuteToolCallUpdate(
105113
title: title,
106114
status: toAcpStatus(item.status),
107115
rawInput: rawInput,
116+
rawOutput: rawOutput,
108117
};
109118
}
110119

120+
export function createMcpRawInput(server: string, tool: string, argumentsValue: JsonValue): Record<string, JsonValue | string> {
121+
const invocation = `Called ${server}.${tool} (${formatJsonInline(argumentsValue)})`;
122+
return {
123+
server,
124+
tool,
125+
arguments: argumentsValue,
126+
invocation,
127+
prettyArguments: formatJsonPretty(argumentsValue),
128+
};
129+
}
130+
131+
export function createMcpRawOutput(
132+
logs: Array<string>,
133+
result: McpToolCallResult | null,
134+
error: McpToolCallError | null,
135+
): Record<string, JsonValue | string | null> | undefined {
136+
const logLines = normalizeMcpLogLines(logs, error, result);
137+
if (logLines.length === 0) {
138+
return undefined;
139+
}
140+
141+
return {
142+
formatted_output: logLines.join("\n\n"),
143+
result,
144+
error,
145+
};
146+
}
147+
148+
function formatMcpResult(result: McpToolCallResult | null): string | null {
149+
if (!result) {
150+
return null;
151+
}
152+
153+
const parts: string[] = [];
154+
if (result.content.length > 0) {
155+
parts.push(result.content.map((contentItem) => formatJsonPretty(contentItem)).join("\n"));
156+
}
157+
if (result.structuredContent !== null) {
158+
parts.push(formatJsonPretty(result.structuredContent));
159+
}
160+
161+
return parts.length > 0 ? parts.join("\n\n") : null;
162+
}
163+
164+
function normalizeMcpLogLines(
165+
logs: Array<string>,
166+
error: McpToolCallError | null,
167+
result: McpToolCallResult | null,
168+
): Array<string> {
169+
const lines: string[] = [];
170+
const seen = new Set<string>();
171+
172+
const append = (value: string | null | undefined) => {
173+
const cleaned = value?.trim();
174+
if (!cleaned || seen.has(cleaned)) {
175+
return;
176+
}
177+
seen.add(cleaned);
178+
lines.push(cleaned);
179+
};
180+
181+
for (const log of logs) {
182+
append(log);
183+
}
184+
append(error?.message);
185+
append(formatMcpResult(result));
186+
187+
return lines;
188+
}
189+
190+
function formatJsonInline(value: JsonValue): string {
191+
return JSON.stringify(value);
192+
}
193+
194+
function formatJsonPretty(value: JsonValue): string {
195+
return JSON.stringify(value, null, 2);
196+
}
197+
111198
export function fuzzyFileSearchToolCallId(sessionId: string): string {
112199
return `fuzzyFileSearch.${sessionId}`;
113200
}

src/__tests__/CodexACPAgent/command-action-events.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,64 @@ describe('CodexEventHandler - command action events', () => {
260260
);
261261
});
262262

263+
it('should include mcp progress and final logs', async () => {
264+
const notifications: ServerNotification[] = [
265+
{
266+
method: 'item/started',
267+
params: {
268+
threadId: 'thread-1',
269+
turnId: 'turn-1',
270+
item: {
271+
type: "mcpToolCall",
272+
id: "call-id",
273+
server: "ijproxy",
274+
tool: "read_file",
275+
status: "inProgress",
276+
arguments: { file_path: ".ai/local.md", mode: "slice", start_line: 1, max_lines: 200 },
277+
result: null,
278+
error: null,
279+
durationMs: null,
280+
},
281+
},
282+
},
283+
{
284+
method: 'item/mcpToolCall/progress',
285+
params: {
286+
threadId: 'thread-1',
287+
turnId: 'turn-1',
288+
itemId: 'call-id',
289+
message: "File /Users/aleksandr.slapoguzov/Projects/ultimate/.ai/local.md doesn't exist or can't be opened",
290+
},
291+
},
292+
{
293+
method: 'item/completed',
294+
params: {
295+
threadId: 'thread-1',
296+
turnId: 'turn-1',
297+
item: {
298+
type: "mcpToolCall",
299+
id: "call-id",
300+
server: "ijproxy",
301+
tool: "read_file",
302+
status: "failed",
303+
arguments: { file_path: ".ai/local.md", mode: "slice", start_line: 1, max_lines: 200 },
304+
result: null,
305+
error: {
306+
message: "File /Users/aleksandr.slapoguzov/Projects/ultimate/.ai/local.md doesn't exist or can't be opened",
307+
},
308+
durationMs: 15,
309+
},
310+
},
311+
},
312+
];
313+
314+
await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications);
315+
316+
await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot(
317+
'data/mcp-tool-completed-with-logs.json'
318+
);
319+
});
320+
263321
it('should handle dynamic tools', async () => {
264322
const dynamicToolNotification: ServerNotification = {
265323
method: 'item/started',

src/__tests__/CodexACPAgent/data/load-session-history.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,14 @@
158158
"toolCallId": "item-mcp-1",
159159
"kind": "execute",
160160
"title": "mcp.github.search",
161-
"status": "completed"
161+
"status": "completed",
162+
"rawInput": {
163+
"server": "github",
164+
"tool": "search",
165+
"arguments": {},
166+
"invocation": "Called github.search ({})",
167+
"prettyArguments": "{}"
168+
}
162169
}
163170
}
164171
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"method": "sessionUpdate",
3+
"args": [
4+
{
5+
"sessionId": "test-session-id",
6+
"update": {
7+
"sessionUpdate": "tool_call_update",
8+
"toolCallId": "call-id",
9+
"_meta": {
10+
"mcp_output_delta": {
11+
"data": "File /Users/aleksandr.slapoguzov/Projects/ultimate/.ai/local.md doesn't exist or can't be opened"
12+
}
13+
}
14+
}
15+
}
16+
]
17+
}
18+
{
19+
"method": "sessionUpdate",
20+
"args": [
21+
{
22+
"sessionId": "test-session-id",
23+
"update": {
24+
"sessionUpdate": "tool_call_update",
25+
"toolCallId": "call-id",
26+
"status": "failed",
27+
"rawInput": {
28+
"server": "ijproxy",
29+
"tool": "read_file",
30+
"arguments": {
31+
"file_path": ".ai/local.md",
32+
"mode": "slice",
33+
"start_line": 1,
34+
"max_lines": 200
35+
},
36+
"invocation": "Called ijproxy.read_file ({\"file_path\":\".ai/local.md\",\"mode\":\"slice\",\"start_line\":1,\"max_lines\":200})",
37+
"prettyArguments": "{\n \"file_path\": \".ai/local.md\",\n \"mode\": \"slice\",\n \"start_line\": 1,\n \"max_lines\": 200\n}"
38+
},
39+
"rawOutput": {
40+
"formatted_output": "File /Users/aleksandr.slapoguzov/Projects/ultimate/.ai/local.md doesn't exist or can't be opened",
41+
"result": null,
42+
"error": {
43+
"message": "File /Users/aleksandr.slapoguzov/Projects/ultimate/.ai/local.md doesn't exist or can't be opened"
44+
}
45+
}
46+
}
47+
}
48+
]
49+
}
50+
{
51+
"method": "sessionUpdate",
52+
"args": [
53+
{
54+
"sessionId": "test-session-id",
55+
"update": {
56+
"sessionUpdate": "tool_call",
57+
"toolCallId": "call-id",
58+
"kind": "execute",
59+
"title": "mcp.ijproxy.read_file",
60+
"status": "in_progress",
61+
"rawInput": {
62+
"server": "ijproxy",
63+
"tool": "read_file",
64+
"arguments": {
65+
"file_path": ".ai/local.md",
66+
"mode": "slice",
67+
"start_line": 1,
68+
"max_lines": 200
69+
},
70+
"invocation": "Called ijproxy.read_file ({\"file_path\":\".ai/local.md\",\"mode\":\"slice\",\"start_line\":1,\"max_lines\":200})",
71+
"prettyArguments": "{\n \"file_path\": \".ai/local.md\",\n \"mode\": \"slice\",\n \"start_line\": 1,\n \"max_lines\": 200\n}"
72+
}
73+
}
74+
}
75+
]
76+
}

0 commit comments

Comments
 (0)