Skip to content

Commit 0c358d3

Browse files
0.118.0 rebase, update logic
1 parent 3a71ad9 commit 0c358d3

5 files changed

Lines changed: 373 additions & 3 deletions

File tree

src/CodexAcpClient.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import open from "open";
77
import type {Disposable} from "vscode-jsonrpc";
88
import type {
99
ClientInfo,
10+
McpStartupCompleteEvent,
1011
ReasoningEffort,
1112
ServerNotification
1213
} from "./app-server";
@@ -21,6 +22,7 @@ import type {
2122
GetAccountResponse,
2223
ListMcpServerStatusParams,
2324
ListMcpServerStatusResponse,
25+
McpServerStatusUpdatedNotification,
2426
Model,
2527
SkillsListParams,
2628
SkillsListResponse,
@@ -275,6 +277,22 @@ export class CodexAcpClient {
275277
return startup.ready;
276278
}
277279

280+
async awaitMcpStartupResult(mcpStartupVersion: number): Promise<McpStartupCompleteEvent> {
281+
return await this.codexClient.awaitMcpStartup(mcpStartupVersion);
282+
}
283+
284+
onMcpServerStatusUpdated(handler: (event: McpServerStatusUpdatedNotification) => void): void {
285+
this.codexClient.onMcpServerStatusUpdated(handler);
286+
}
287+
288+
getMcpServerStatusVersion(): number {
289+
return this.codexClient.getMcpServerStatusVersion();
290+
}
291+
292+
getMcpServerStatusUpdates(afterVersion: number): Array<McpServerStatusUpdatedNotification> {
293+
return this.codexClient.getMcpServerStatusUpdates(afterVersion);
294+
}
295+
278296
getMcpStartupCompleteVersion(): number {
279297
return this.codexClient.getMcpStartupCompleteVersion();
280298
}

src/CodexAcpServer.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,18 @@ 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, Thread, ThreadItem, UserInput, ReasoningEffortOption} from "./app-server/v2";
13+
import type {McpStartupCompleteEvent, InputModality, ReasoningEffort} from "./app-server";
14+
import type {
15+
Account,
16+
CollabAgentToolCallStatus,
17+
McpServerStatusUpdatedNotification,
18+
Model,
19+
Thread,
20+
ThreadItem,
21+
UserInput,
22+
ReasoningEffortOption
23+
} from "./app-server/v2";
1424
import type {RateLimitsMap} from "./RateLimitsMap";
15-
import type {InputModality, ReasoningEffort} from "./app-server";
1625
import {ModelId} from "./ModelId";
1726
import {AgentMode} from "./AgentMode";
1827
import type {TokenCount} from "./TokenCount";
@@ -43,6 +52,12 @@ export interface SessionState {
4352
sessionMcpServers?: Array<string>;
4453
}
4554

55+
interface PendingMcpStartupSession {
56+
requestedServers: Set<string>;
57+
terminalServers: Set<string>;
58+
publishedFailures: Set<string>;
59+
}
60+
4661
export class CodexAcpServer implements acp.Agent {
4762
private readonly codexAcpClient: CodexAcpClient;
4863
private readonly connection: acp.AgentSideConnection;
@@ -51,6 +66,7 @@ export class CodexAcpServer implements acp.Agent {
5166
private readonly availableCommands: CodexCommands;
5267

5368
private readonly sessions: Map<string, SessionState>;
69+
private readonly pendingMcpStartupSessions: Map<string, PendingMcpStartupSession>;
5470

5571
constructor(
5672
connection: acp.AgentSideConnection,
@@ -59,6 +75,7 @@ export class CodexAcpServer implements acp.Agent {
5975
getExitCode?: () => number | null,
6076
) {
6177
this.sessions = new Map();
78+
this.pendingMcpStartupSessions = new Map();
6279
this.connection = connection;
6380
this.codexAcpClient = codexAcpClient;
6481
this.defaultAuthRequest = defaultAuthRequest ?? null;
@@ -68,6 +85,9 @@ export class CodexAcpServer implements acp.Agent {
6885
codexAcpClient,
6986
(operation) => this.runWithProcessCheck(operation)
7087
);
88+
this.codexAcpClient.onMcpServerStatusUpdated((event) => {
89+
void this.handleMcpServerStatusUpdated(event);
90+
});
7191
}
7292

7393
async initialize(
@@ -128,6 +148,7 @@ export class CodexAcpServer implements acp.Agent {
128148
async getOrCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, SessionModelState, SessionModeState]> {
129149
await this.checkAuthorization();
130150
const mcpStartupVersion = this.codexAcpClient.getMcpStartupCompleteVersion();
151+
const mcpStatusVersion = this.codexAcpClient.getMcpServerStatusVersion();
131152

132153
let sessionMetadata: SessionMetadata;
133154
if ("sessionId" in request) {
@@ -159,6 +180,17 @@ export class CodexAcpServer implements acp.Agent {
159180
}
160181
this.sessions.set(sessionId, sessionState);
161182

183+
const requestedMcpServers = request.mcpServers ?? [];
184+
if (requestedMcpServers.length > 0) {
185+
this.pendingMcpStartupSessions.set(sessionId, {
186+
requestedServers: new Set(requestedMcpServers.map(server => server.name)),
187+
terminalServers: new Set(),
188+
publishedFailures: new Set(),
189+
});
190+
await this.replayBufferedMcpStartupStatus(sessionId, mcpStatusVersion);
191+
this.publishMcpStartupStatusAsync(sessionId, mcpStartupVersion);
192+
}
193+
162194
this.publishAvailableCommandsAsync(sessionId);
163195
const sessionModelState: SessionModelState = this.createModelState(models, currentModelId);
164196
const sessionModeState: SessionModeState = sessionState.agentMode.toSessionModeState();
@@ -621,6 +653,80 @@ export class CodexAcpServer implements acp.Agent {
621653
return await this.runWithProcessCheck(() => this.codexAcpClient.awaitMcpStartup(mcpStartupVersion));
622654
}
623655

656+
private publishMcpStartupStatusAsync(sessionId: string, mcpStartupVersion: number): void {
657+
void (async () => {
658+
try {
659+
const mcpStartup = await this.runWithProcessCheck(() => this.codexAcpClient.awaitMcpStartupResult(mcpStartupVersion));
660+
const sessionState = this.sessions.get(sessionId);
661+
if (sessionState) {
662+
sessionState.sessionMcpServers = mcpStartup.ready;
663+
}
664+
await this.publishMcpStartupStatus(sessionId, mcpStartup);
665+
this.pendingMcpStartupSessions.delete(sessionId);
666+
} catch (err) {
667+
logger.error(`Failed to publish MCP startup status for session ${sessionId}`, err);
668+
}
669+
})();
670+
}
671+
672+
private async publishMcpStartupStatus(sessionId: string, mcpStartup: McpStartupCompleteEvent): Promise<void> {
673+
for (const update of CodexEventHandler.createMcpStartupUpdates(mcpStartup)) {
674+
await this.connection.sessionUpdate({
675+
sessionId,
676+
update,
677+
});
678+
}
679+
}
680+
681+
private async handleMcpServerStatusUpdated(event: McpServerStatusUpdatedNotification): Promise<void> {
682+
for (const [sessionId, pending] of this.pendingMcpStartupSessions) {
683+
if (!pending.requestedServers.has(event.name)) {
684+
continue;
685+
}
686+
687+
if (event.status === "ready") {
688+
pending.terminalServers.add(event.name);
689+
const sessionState = this.sessions.get(sessionId);
690+
if (sessionState && !(sessionState.sessionMcpServers ?? []).includes(event.name)) {
691+
sessionState.sessionMcpServers = [...(sessionState.sessionMcpServers ?? []), event.name];
692+
}
693+
} else if (event.status === "failed" || event.status === "cancelled") {
694+
pending.terminalServers.add(event.name);
695+
if (!pending.publishedFailures.has(event.name)) {
696+
pending.publishedFailures.add(event.name);
697+
const updates = CodexEventHandler.createMcpStartupUpdates({
698+
ready: [],
699+
failed: event.status === "failed" ? [{
700+
server: event.name,
701+
error: event.error ?? "Unknown MCP startup error",
702+
}] : [],
703+
cancelled: event.status === "cancelled" ? [event.name] : [],
704+
});
705+
for (const update of updates) {
706+
await this.connection.sessionUpdate({
707+
sessionId,
708+
update,
709+
});
710+
}
711+
}
712+
}
713+
714+
if (pending.terminalServers.size >= pending.requestedServers.size) {
715+
this.pendingMcpStartupSessions.delete(sessionId);
716+
}
717+
}
718+
}
719+
720+
private async replayBufferedMcpStartupStatus(sessionId: string, afterVersion: number): Promise<void> {
721+
const updates = this.codexAcpClient.getMcpServerStatusUpdates(afterVersion);
722+
for (const update of updates) {
723+
await this.handleMcpServerStatusUpdated(update);
724+
if (!this.pendingMcpStartupSessions.has(sessionId)) {
725+
return;
726+
}
727+
}
728+
}
729+
624730
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
625731
logger.log("Prompt received", {
626732
sessionId: params.sessionId,

src/CodexAppServerClient.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
GetAccountParams,
1212
GetAccountResponse, LoginAccountParams, LoginAccountResponse, LogoutAccountResponse, ModelListParams,
1313
ModelListResponse,
14+
McpServerStatusUpdatedNotification,
1415
ThreadStartParams,
1516
ThreadStartResponse,
1617
ThreadLoadedListParams,
@@ -63,6 +64,12 @@ export class CodexAppServerClient {
6364
private mcpStartupCompleteVersion = 0;
6465
private lastMcpStartupComplete: McpStartupCompleteEvent | null = null;
6566
private readonly mcpStartupCompleteResolvers: Array<SignalResolver<McpStartupCompleteEvent>> = [];
67+
private mcpServerStatusVersion = 0;
68+
private readonly mcpServerStatusUpdatedHandlers: Array<(event: McpServerStatusUpdatedNotification) => void> = [];
69+
private readonly mcpServerStatusHistory: Array<{
70+
version: number;
71+
event: McpServerStatusUpdatedNotification;
72+
}> = [];
6673

6774
constructor(connection: MessageConnection) {
6875
this.connection = connection;
@@ -76,8 +83,10 @@ export class CodexAppServerClient {
7683
}
7784
return;
7885
}
79-
8086
const serverNotification = data as ServerNotification;
87+
if (serverNotification.method === "mcpServer/startupStatus/updated") {
88+
this.recordMcpServerStatusUpdated(serverNotification.params);
89+
}
8190
this.notify(serverNotification);
8291
for (const callback of this.codexEventHandlers) {
8392
callback({ eventType: "notification", ...serverNotification });
@@ -166,6 +175,20 @@ export class CodexAppServerClient {
166175
);
167176
}
168177

178+
onMcpServerStatusUpdated(handler: (event: McpServerStatusUpdatedNotification) => void): void {
179+
this.mcpServerStatusUpdatedHandlers.push(handler);
180+
}
181+
182+
getMcpServerStatusVersion(): number {
183+
return this.mcpServerStatusVersion;
184+
}
185+
186+
getMcpServerStatusUpdates(afterVersion: number): Array<McpServerStatusUpdatedNotification> {
187+
return this.mcpServerStatusHistory
188+
.filter(entry => entry.version > afterVersion)
189+
.map(entry => entry.event);
190+
}
191+
169192
async accountRead(params: GetAccountParams): Promise<GetAccountResponse> {
170193
return await this.sendRequest({ method: "account/read", params: params });
171194
}
@@ -237,6 +260,17 @@ export class CodexAppServerClient {
237260
});
238261
}
239262

263+
private recordMcpServerStatusUpdated(event: McpServerStatusUpdatedNotification): void {
264+
this.mcpServerStatusVersion += 1;
265+
this.mcpServerStatusHistory.push({
266+
version: this.mcpServerStatusVersion,
267+
event,
268+
});
269+
for (const handler of this.mcpServerStatusUpdatedHandlers) {
270+
handler(event);
271+
}
272+
}
273+
240274
private async sendRequest<R>(request: CodexRequest): Promise<R> {
241275
for (const callback of this.codexEventHandlers) {
242276
callback({ eventType: "request", ...request});

src/CodexEventHandler.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
ThreadTokenUsageUpdatedNotification,
2121
TurnPlanUpdatedNotification
2222
} from "./app-server/v2";
23+
import type { McpStartupCompleteEvent } from "./app-server";
2324
import {toTokenCount} from "./TokenCount";
2425
import {
2526
createCommandExecutionUpdate,
@@ -258,6 +259,40 @@ export class CodexEventHandler {
258259
}
259260
}
260261

262+
static createMcpStartupUpdates(event: McpStartupCompleteEvent): UpdateSessionEvent[] {
263+
const failedUpdates = event.failed.map((server: McpStartupCompleteEvent["failed"][number]) => this.createMcpStartupToolCallUpdate(
264+
server.server,
265+
`[codex-acp forwarded startup error] MCP server \`${server.server}\` failed to start: ${server.error}`
266+
));
267+
const cancelledUpdates = event.cancelled.map((server: McpStartupCompleteEvent["cancelled"][number]) => this.createMcpStartupToolCallUpdate(
268+
server,
269+
`[codex-acp forwarded startup error] MCP server \`${server}\` startup was cancelled.`
270+
));
271+
272+
return [...failedUpdates, ...cancelledUpdates];
273+
}
274+
275+
private static createMcpStartupToolCallUpdate(serverName: string, message: string): UpdateSessionEvent {
276+
return {
277+
sessionUpdate: "tool_call",
278+
toolCallId: this.getMcpStartupToolCallId(serverName),
279+
kind: "other",
280+
title: `mcp__${serverName}__startup`,
281+
status: "failed",
282+
content: [{
283+
type: "content",
284+
content: {
285+
type: "text",
286+
text: message,
287+
},
288+
}],
289+
};
290+
}
291+
292+
private static getMcpStartupToolCallId(serverName: string): string {
293+
return `mcp_startup.${encodeURIComponent(serverName)}`;
294+
}
295+
261296
private completeCommandExecutionEvent(item: ThreadItem & { "type": "commandExecution" }): UpdateSessionEvent {
262297
return {
263298
sessionUpdate: "tool_call_update",

0 commit comments

Comments
 (0)