diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index bc70c5bf7a161..0e7ba5f227e88 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -1283,6 +1283,35 @@ describe("ClaudeAdapterV2 MCP elicitation acceptance", () => { } }); + // mongodb-mcp-server's Elicitation.CONFIRMATION_SCHEMA (@mongodb-js/mcp-core 3.2.2). + it("answers a Yes/No confirmation form with the affirmative choice", () => { + assert.deepEqual( + ClaudeAdapterV2.claudeMcpElicitationAcceptance( + form( + { + confirmation: { + type: "string", + title: "Would you like to confirm?", + description: "Would you like to confirm?", + enum: ["Yes", "No"], + enumNames: ["Yes, I confirm", "No, I do not confirm"], + }, + }, + ["confirmation"], + ), + ), + { action: "accept", content: { confirmation: "Yes" } }, + ); + for (const choice of ["yes", "Confirm", "Proceed", "OK", "continue"]) { + assert.deepEqual( + ClaudeAdapterV2.claudeMcpElicitationAcceptance( + form({ answer: { type: "string", enum: ["No", "Cancel", choice] } }, ["answer"]), + ), + { action: "accept", content: { answer: choice } }, + ); + } + }); + it("answers a persistence toggle with false", () => { assert.deepEqual( ClaudeAdapterV2.claudeMcpElicitationAcceptance( @@ -1314,6 +1343,16 @@ describe("ClaudeAdapterV2 MCP elicitation acceptance", () => { form({ grant: { type: "string", enum: ["always", "session"] } }, ["grant"]), form({ grant: { type: "string", enum: ["deny", "always"], default: "always" } }, ["grant"]), form({ limit: { type: "number", default: 500 } }, ["limit"]), + form({ confirmation: { type: "string", enum: ["No", "Cancel"], default: "No" } }, [ + "confirmation", + ]), + form({ grant: { type: "string", enum: ["No", "Yes always", "yes_for_session"] } }, ["grant"]), + form( + { + remember: { type: "string", title: "Always allow this application", enum: ["Yes", "No"] }, + }, + ["remember"], + ), { serverName: "deploys", message: "Deploy web?", mode: "form" }, ] satisfies ReadonlyArray) { assert.isUndefined(ClaudeAdapterV2.claudeMcpElicitationAcceptance(request)); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 3c6295494ec83..f27b51f75496f 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3855,6 +3855,7 @@ export function claudeMcpElicitationAcceptance( const response = toMcpElicitationResponse( { mode: "form", requestedSchema: withoutSchemaDefaults(request.requestedSchema) }, "accept", + { acceptConfirmationChoices: true }, ); // The helper leaves content out when the schema is not a form it can read. const filled = response.content; diff --git a/apps/server/src/provider/CodexMcpElicitation.ts b/apps/server/src/provider/CodexMcpElicitation.ts index eafe09e07e706..07af9a7d0f904 100644 --- a/apps/server/src/provider/CodexMcpElicitation.ts +++ b/apps/server/src/provider/CodexMcpElicitation.ts @@ -177,9 +177,16 @@ export function describeMcpElicitation( }; } +const ONE_TIME_APPROVAL_CHOICE = + /^(?:once|accept|approve|allow)(?:[_\s-]*(?:once(?:[_\s-]*only)?|this[_\s-]*time))?$/i; +// Confirmation forms (e.g. mongodb-mcp-server's required "Yes"/"No" enum) ask +// for consent with a plain affirmative instead of an allow-once choice. +const ONE_TIME_CONFIRMATION_CHOICE = /^(?:yes|confirm|proceed|continue|ok(?:ay)?)$/i; + export function toMcpElicitationResponse( payload: McpElicitationResponseInput, decision: ProviderApprovalDecision, + responseOptions?: { readonly acceptConfirmationChoices?: boolean }, ): EffectCodexSchema.McpServerElicitationRequestResponse { if (decision === "decline" || decision === "cancel") { return { action: decision }; @@ -203,9 +210,11 @@ export function toMcpElicitationResponse( const chosenOption = options.find((option) => persist ? mcpElicitationPersistenceDecision(option.value) === decision - : /^(?:once|accept|approve|allow)(?:[_\s-]*(?:once(?:[_\s-]*only)?|this[_\s-]*time))?$/i.test( - option.value.trim(), - ) && mcpElicitationPersistenceDecision(option.value) === null, + : (ONE_TIME_APPROVAL_CHOICE.test(option.value.trim()) || + (responseOptions?.acceptConfirmationChoices === true && + !isMcpElicitationPersistenceField(key, field) && + ONE_TIME_CONFIRMATION_CHOICE.test(option.value.trim()))) && + mcpElicitationPersistenceDecision(option.value) === null, ); if (chosenOption) { content[key] = chosenOption.value;