@@ -5,8 +5,11 @@ import type {
55 CommandExecutionRequestApprovalParams ,
66 CommandExecutionRequestApprovalResponse ,
77 FileChangeRequestApprovalParams ,
8- FileChangeRequestApprovalResponse
8+ FileChangeRequestApprovalResponse ,
9+ McpServerElicitationRequestParams ,
10+ McpServerElicitationRequestResponse
911} from "./app-server/v2" ;
12+ import type { JsonValue } from "./app-server/serde_json/JsonValue" ;
1013import type { ToolCallContent } from "@agentclientprotocol/sdk/dist/schema/types.gen" ;
1114import { logger } from "./Logger" ;
1215import { stripShellPrefix } from "./CodexEventHandler" ;
@@ -17,6 +20,14 @@ const APPROVAL_OPTIONS: acp.PermissionOption[] = [
1720 { optionId : "reject_once" , name : "Reject" , kind : "reject_once" } ,
1821] ;
1922
23+ const ELICITATION_ALLOW_ONCE_OPTION_ID = "allow_once" ;
24+ const ELICITATION_ALLOW_SESSION_OPTION_ID = "allow_for_session" ;
25+ const ELICITATION_ALLOW_ALWAYS_OPTION_ID = "allow_always" ;
26+ const ELICITATION_DENY_ONCE_OPTION_ID = "deny_once" ;
27+
28+ type ElicitationPersistOption = "session" | "always" ;
29+ type JsonObject = { [ key : string ] : JsonValue } ;
30+
2031export class CodexApprovalHandler implements ApprovalHandler {
2132 private readonly connection : acp . AgentSideConnection ;
2233 private readonly sessionState : SessionState ;
@@ -57,11 +68,25 @@ export class CodexApprovalHandler implements ApprovalHandler {
5768 }
5869 }
5970
71+ async handleMcpServerElicitation (
72+ params : McpServerElicitationRequestParams
73+ ) : Promise < McpServerElicitationRequestResponse > {
74+ try {
75+ const sessionId = this . sessionState . sessionId ;
76+ const acpRequest = this . buildMcpServerElicitationPermissionRequest ( sessionId , params ) ;
77+ const response = await this . connection . requestPermission ( acpRequest ) ;
78+ return this . convertMcpServerElicitationResponse ( params , response ) ;
79+ } catch ( error ) {
80+ logger . error ( "Error requesting MCP server elicitation approval" , error ) ;
81+ return this . createCancelledMcpServerElicitationResponse ( ) ;
82+ }
83+ }
84+
6085 private buildCommandPermissionRequest (
6186 sessionId : string ,
6287 params : CommandExecutionRequestApprovalParams
6388 ) : acp . RequestPermissionRequest {
64- const reasonContent = this . createContentFromReason ( params . reason ?? null ) ;
89+ const reasonContent = this . createTextContent ( params . reason ?? null ) ;
6590 return {
6691 sessionId,
6792 toolCall : {
@@ -75,24 +100,31 @@ export class CodexApprovalHandler implements ApprovalHandler {
75100 } ;
76101 }
77102
78- private createContentFromReason ( reason : string | null ) : ToolCallContent | null {
79- if ( reason === null || reason === "" ) {
103+ private createTextContent ( text : string | null ) : ToolCallContent | null {
104+ if ( text === null || text === "" ) {
80105 return null ;
81106 }
82107 return {
83108 type : "content" ,
84109 content : {
85110 type : "text" ,
86- text : reason
111+ text
87112 }
88113 }
89114 }
90115
116+ private createTextContents ( ...texts : Array < string | null | undefined > ) : Array < ToolCallContent > | null {
117+ const contents = texts
118+ . map ( text => this . createTextContent ( text ?? null ) )
119+ . filter ( ( content ) : content is ToolCallContent => content !== null )
120+ return contents . length > 0 ? contents : null
121+ }
122+
91123 private buildFileChangePermissionRequest (
92124 sessionId : string ,
93125 params : FileChangeRequestApprovalParams
94126 ) : acp . RequestPermissionRequest {
95- const reasonContent = this . createContentFromReason ( params . reason ?? null ) ;
127+ const reasonContent = this . createTextContent ( params . reason ?? null ) ;
96128 return {
97129 sessionId,
98130 toolCall : {
@@ -105,6 +137,117 @@ export class CodexApprovalHandler implements ApprovalHandler {
105137 } ;
106138 }
107139
140+ private buildMcpServerElicitationPermissionRequest (
141+ sessionId : string ,
142+ params : McpServerElicitationRequestParams
143+ ) : acp . RequestPermissionRequest {
144+ const meta = this . asRecord ( params . _meta ) ;
145+ const persist = meta ?. [ "persist" ] ;
146+ const persistOptions = ( Array . isArray ( persist ) ? persist : [ persist ] ) . filter (
147+ ( value ) : value is ElicitationPersistOption => value === "session" || value === "always"
148+ ) ;
149+ const toolDescription = typeof meta ?. [ "tool_description" ] === "string"
150+ ? meta [ "tool_description" ]
151+ : null ;
152+ const rawInput = this . tryToJsonValue ( meta ?. [ "tool_params" ] ) ?? null ;
153+
154+ return {
155+ sessionId,
156+ toolCall : {
157+ toolCallId : this . buildMcpServerElicitationToolCallId ( params ) ,
158+ title : params . message !== "" ? params . message : "MCP permission request" ,
159+ kind : "other" ,
160+ status : "pending" ,
161+ content : this . createTextContents ( toolDescription ) ,
162+ rawInput,
163+ } ,
164+ options : this . buildMcpServerElicitationOptions ( persistOptions ) ,
165+ } ;
166+ }
167+
168+ private buildMcpServerElicitationToolCallId (
169+ params : McpServerElicitationRequestParams
170+ ) : string {
171+ return `mcp-elicitation:${ params . serverName } :${ params . turnId ?? params . threadId } :${ params . mode } ` ;
172+ }
173+
174+ private buildMcpServerElicitationOptions (
175+ persistOptions : Array < ElicitationPersistOption >
176+ ) : Array < acp . PermissionOption > {
177+ const options : Array < acp . PermissionOption > = [
178+ { optionId : ELICITATION_ALLOW_ONCE_OPTION_ID , name : "Allow Once" , kind : "allow_once" } ,
179+ ]
180+
181+ if ( persistOptions . includes ( "session" ) ) {
182+ options . push ( {
183+ optionId : ELICITATION_ALLOW_SESSION_OPTION_ID ,
184+ name : "Allow for Session" ,
185+ kind : "allow_always" ,
186+ } )
187+ }
188+
189+ if ( persistOptions . includes ( "always" ) ) {
190+ options . push ( {
191+ optionId : ELICITATION_ALLOW_ALWAYS_OPTION_ID ,
192+ name : "Always Allow" ,
193+ kind : "allow_always" ,
194+ } )
195+ }
196+
197+ options . push ( {
198+ optionId : ELICITATION_DENY_ONCE_OPTION_ID ,
199+ name : "Deny Once" ,
200+ kind : "reject_once" ,
201+ } )
202+
203+ return options
204+ }
205+
206+ private asRecord ( value : unknown ) : Record < string , unknown > | null {
207+ if ( value === null || typeof value !== "object" || Array . isArray ( value ) ) {
208+ return null
209+ }
210+ return value as Record < string , unknown >
211+ }
212+
213+ private tryToJsonValue ( value : unknown ) : JsonValue | undefined {
214+ if (
215+ value === null ||
216+ typeof value === "string" ||
217+ typeof value === "number" ||
218+ typeof value === "boolean"
219+ ) {
220+ return value
221+ }
222+
223+ if ( Array . isArray ( value ) ) {
224+ const convertedValues : Array < JsonValue > = [ ]
225+ for ( const item of value ) {
226+ const convertedItem = this . tryToJsonValue ( item )
227+ if ( convertedItem === undefined ) {
228+ return undefined
229+ }
230+ convertedValues . push ( convertedItem )
231+ }
232+ return convertedValues
233+ }
234+
235+ if ( typeof value === "object" ) {
236+ const record = value as Record < string , unknown >
237+ const jsonObject : JsonObject = { }
238+ for ( const [ key , nestedValue ] of Object . entries ( record ) ) {
239+ const convertedValue = this . tryToJsonValue ( nestedValue )
240+ if ( convertedValue === undefined ) {
241+ return undefined
242+ }
243+ jsonObject [ key ] = convertedValue
244+ }
245+ return jsonObject
246+ }
247+
248+ return undefined
249+ }
250+
108251 private convertCommandResponse (
109252 response : acp . RequestPermissionResponse
110253 ) : CommandExecutionRequestApprovalResponse {
@@ -138,4 +281,42 @@ export class CodexApprovalHandler implements ApprovalHandler {
138281 return { decision : "cancel" } ;
139282 }
140283 }
284+
285+ private convertMcpServerElicitationResponse (
286+ params : McpServerElicitationRequestParams ,
287+ response : acp . RequestPermissionResponse
288+ ) : McpServerElicitationRequestResponse {
289+ if ( response . outcome . outcome === "cancelled" ) {
290+ return this . createCancelledMcpServerElicitationResponse ( ) ;
291+ }
292+
293+ switch ( response . outcome . optionId ) {
294+ case ELICITATION_ALLOW_ONCE_OPTION_ID :
295+ return this . createAcceptedMcpServerElicitationResponse ( params , null ) ;
296+ case ELICITATION_ALLOW_SESSION_OPTION_ID :
297+ return this . createAcceptedMcpServerElicitationResponse ( params , { persist : "session" } ) ;
298+ case ELICITATION_ALLOW_ALWAYS_OPTION_ID :
299+ return this . createAcceptedMcpServerElicitationResponse ( params , { persist : "always" } ) ;
300+ case ELICITATION_DENY_ONCE_OPTION_ID :
301+ case "reject_once" :
302+ return { action : "decline" , content : null , _meta : null } ;
303+ default :
304+ return this . createCancelledMcpServerElicitationResponse ( ) ;
305+ }
306+ }
307+
308+ private createAcceptedMcpServerElicitationResponse (
309+ params : McpServerElicitationRequestParams ,
310+ meta : JsonObject | null
311+ ) : McpServerElicitationRequestResponse {
312+ return {
313+ action : "accept" ,
314+ content : params . mode === "form" ? { } : null ,
315+ _meta : meta ,
316+ }
317+ }
318+
319+ private createCancelledMcpServerElicitationResponse ( ) : McpServerElicitationRequestResponse {
320+ return { action : "cancel" , content : null , _meta : null }
321+ }
141322}
0 commit comments