Skip to content

Commit ecc3de4

Browse files
LLM-26016 [Codex] Show mcp initialization errors in chat
1 parent b441398 commit ecc3de4

6 files changed

Lines changed: 117 additions & 12 deletions

File tree

package-lock.json

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/CodexAcpClient.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,8 @@ export class CodexAcpClient {
259259
};
260260
}
261261

262-
async awaitMcpServers(): Promise<Array<string>>{
263-
const response = await this.codexClient.awaitMcpStartup();
264-
return response.ready;
262+
async awaitMcpServers() {
263+
return await this.codexClient.awaitMcpStartup();
265264
}
266265

267266
private createSessionConfig(projectPath: string, mcpServers: Array<McpServer>): JsonObject {

src/CodexAcpServer.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import {CodexApprovalHandler} from "./CodexApprovalHandler";
1010
import {CodexAuthMethods, type CodexAuthRequest} from "./CodexAuthMethod";
1111
import {CodexAcpClient, type SessionMetadata, type SessionMetadataWithThread} from "./CodexAcpClient";
1212
import {ACPSessionConnection, type UpdateSessionEvent} from "./ACPSessionConnection";
13-
import type {Account, CollabAgentToolCallStatus, Model, RateLimitSnapshot, Thread, ThreadItem, UserInput, ReasoningEffortOption} from "./app-server/v2";
13+
import type {Account, CollabAgentToolCallStatus, Model, Thread, ThreadItem, UserInput, ReasoningEffortOption} from "./app-server/v2";
1414
import type {RateLimitsMap} from "./RateLimitsMap";
1515
import type {InputModality, ReasoningEffort} from "./app-server";
16+
import type {McpStartupCompleteEvent} from "./app-server";
1617
import {ModelId} from "./ModelId";
1718
import {AgentMode} from "./AgentMode";
1819
import type {TokenCount} from "./TokenCount";
@@ -127,7 +128,7 @@ export class CodexAcpServer implements acp.Agent {
127128

128129
async getOrCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, SessionModelState, SessionModeState]> {
129130
await this.checkAuthorization();
130-
const pendingMcpServers: Promise<Array<string>> = this.codexAcpClient.awaitMcpServers();
131+
const pendingMcpStartup: Promise<McpStartupCompleteEvent> = this.codexAcpClient.awaitMcpServers();
131132

132133
let sessionMetadata: SessionMetadata;
133134
if ("sessionId" in request) {
@@ -141,7 +142,9 @@ export class CodexAcpServer implements acp.Agent {
141142
const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
142143
const {sessionId, currentModelId, models} = sessionMetadata;
143144
logger.log(`Waiting MCP servers to start...`)
144-
const sessionMcpServers = await pendingMcpServers;
145+
const mcpStartup = await pendingMcpStartup;
146+
const sessionMcpServers = mcpStartup.ready;
147+
await this.publishMcpStartupStatus(sessionId, mcpStartup);
145148
const currentModel = this.findCurrentModel(models, currentModelId);
146149
const sessionState: SessionState = {
147150
sessionId: sessionId,
@@ -332,7 +335,7 @@ export class CodexAcpServer implements acp.Agent {
332335
thread: Thread;
333336
}> {
334337
await this.checkAuthorization();
335-
const pendingMcpServers: Promise<Array<string>> = this.codexAcpClient.awaitMcpServers();
338+
const pendingMcpStartup: Promise<McpStartupCompleteEvent> = this.codexAcpClient.awaitMcpServers();
336339

337340
logger.log(`Load existing session: ${request.sessionId}...`);
338341
const sessionMetadata: SessionMetadataWithThread = await this.runWithProcessCheck(() =>
@@ -342,7 +345,9 @@ export class CodexAcpServer implements acp.Agent {
342345
const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
343346
const {sessionId, currentModelId, models, thread} = sessionMetadata;
344347
logger.log("Waiting MCP servers to start...");
345-
const sessionMcpServers = await pendingMcpServers;
348+
const mcpStartup = await pendingMcpStartup;
349+
const sessionMcpServers = mcpStartup.ready;
350+
await this.publishMcpStartupStatus(sessionId, mcpStartup);
346351
const currentModel = this.findCurrentModel(models, currentModelId);
347352
const sessionState: SessionState = {
348353
sessionId: sessionId,
@@ -700,6 +705,52 @@ export class CodexAcpServer implements acp.Agent {
700705
};
701706
}
702707

708+
private async publishMcpStartupStatus(sessionId: string, mcpStartup: McpStartupCompleteEvent): Promise<void> {
709+
if (mcpStartup.failed.length === 0 && mcpStartup.cancelled.length === 0) {
710+
return;
711+
}
712+
713+
for (const server of mcpStartup.failed) {
714+
await this.connection.sessionUpdate({
715+
sessionId,
716+
update: this.createMcpStartupToolCallUpdate(
717+
server.server,
718+
`[codex-acp forwarded startup error] MCP server \`${server.server}\` failed to start: ${server.error}`
719+
),
720+
});
721+
}
722+
for (const server of mcpStartup.cancelled) {
723+
await this.connection.sessionUpdate({
724+
sessionId,
725+
update: this.createMcpStartupToolCallUpdate(
726+
server,
727+
`[codex-acp forwarded startup error] MCP server \`${server}\` startup was cancelled.`
728+
),
729+
});
730+
}
731+
}
732+
733+
private createMcpStartupToolCallUpdate(serverName: string, message: string): UpdateSessionEvent {
734+
return {
735+
sessionUpdate: "tool_call",
736+
toolCallId: this.getMcpStartupToolCallId(serverName),
737+
kind: "other",
738+
title: `mcp__${serverName}__startup`,
739+
status: "failed",
740+
content: [{
741+
type: "content",
742+
content: {
743+
type: "text",
744+
text: message,
745+
},
746+
}],
747+
};
748+
}
749+
750+
private getMcpStartupToolCallId(serverName: string): string {
751+
return `mcp_startup.${encodeURIComponent(serverName)}`;
752+
}
753+
703754
private async runWithProcessCheck<T>(operation: () => Promise<T>): Promise<T> {
704755
try {
705756
return await operation();

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,48 @@ describe('ACP server test', { timeout: 40_000 }, () => {
135135
expect(newSessionResponse.sessionId).toBeDefined()
136136
})
137137

138+
it('should forward MCP startup failures to ACP session updates', async () => {
139+
const mockFixture = createCodexMockTestFixture();
140+
const codexAcpAgent = mockFixture.getCodexAcpAgent();
141+
const codexAcpClient = mockFixture.getCodexAcpClient();
142+
143+
vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false);
144+
vi.spyOn(codexAcpClient, "newSession").mockResolvedValue({
145+
sessionId: "session-id",
146+
currentModelId: "gpt-5[medium]",
147+
models: [{
148+
id: "gpt-5",
149+
model: "gpt-5",
150+
upgrade: null,
151+
upgradeInfo: null,
152+
availabilityNux: null,
153+
displayName: "GPT-5",
154+
description: "Test model",
155+
hidden: false,
156+
supportedReasoningEfforts: [{ reasoningEffort: "medium", description: "Balanced" }],
157+
defaultReasoningEffort: "medium",
158+
inputModalities: ["text"],
159+
supportsPersonality: false,
160+
isDefault: true,
161+
}],
162+
});
163+
vi.spyOn(codexAcpClient, "getAccount").mockResolvedValue({ account: null, requiresOpenaiAuth: false });
164+
vi.spyOn(codexAcpClient, "awaitMcpServers").mockResolvedValue({
165+
ready: ["athena-jetbrains-mcp"],
166+
failed: [{
167+
server: "ide-athenadiff-mcp",
168+
error: "MCP startup failed: connection closed: initialize response",
169+
}],
170+
cancelled: ["ide-jira-mcp"],
171+
});
172+
173+
await codexAcpAgent.newSession({ cwd: "/workspace", mcpServers: [] });
174+
175+
const dump = mockFixture.getAcpConnectionDump([]);
176+
expect(dump).toContain("MCP server `ide-athenadiff-mcp` failed to start: MCP startup failed: connection closed: initialize response");
177+
expect(dump).toContain("MCP server `ide-jira-mcp` startup was cancelled.");
178+
});
179+
138180
it('prefetches session additional skill roots before thread start', async () => {
139181
const mockFixture = createCodexMockTestFixture();
140182
const codexAcpClient = mockFixture.getCodexAcpClient();

src/__tests__/CodexACPAgent/load-session.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ describe("CodexACPAgent - loadSession", () => {
1515
account: null,
1616
requiresOpenaiAuth: false,
1717
});
18-
codexAcpClient.awaitMcpServers = vi.fn().mockResolvedValue([]);
18+
codexAcpClient.awaitMcpServers = vi.fn().mockResolvedValue({
19+
ready: [],
20+
failed: [],
21+
cancelled: [],
22+
});
1923
codexAcpClient.listSkills = vi.fn().mockResolvedValue({ data: [] });
2024

2125
const model: Model = {

src/__tests__/CodexACPAgent/model-filtering.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ describe("Model filtering", () => {
8282
models,
8383
});
8484
vi.spyOn(codexAcpClient, "getAccount").mockResolvedValue({account: null, requiresOpenaiAuth: false});
85-
vi.spyOn(codexAcpClient, "awaitMcpServers").mockResolvedValue([]);
85+
vi.spyOn(codexAcpClient, "awaitMcpServers").mockResolvedValue({
86+
ready: [],
87+
failed: [],
88+
cancelled: [],
89+
});
8690

8791
const newSessionResponse = await codexAcpAgent.newSession({ cwd: "", mcpServers: [] });
8892
const sessionModels = newSessionResponse.models;

0 commit comments

Comments
 (0)