Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<ElicitationRequest>) {
assert.isUndefined(ClaudeAdapterV2.claudeMcpElicitationAcceptance(request));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 12 additions & 3 deletions apps/server/src/provider/CodexMcpElicitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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,
Comment on lines +213 to +217

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium provider/CodexMcpElicitation.ts:213

With acceptConfirmationChoices enabled, ordinary Approve sends { persist: "Yes" } for a string persistence field with enum ["Yes", "No"], silently granting an unseen persistent permission. The confirmation match checks only the option text and does not use isMcpElicitationPersistenceField for string fields; exclude persistence fields from this match or choose their negative option.

Suggested change
: (ONE_TIME_APPROVAL_CHOICE.test(option.value.trim()) ||
(responseOptions?.acceptConfirmationChoices === true &&
ONE_TIME_CONFIRMATION_CHOICE.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,
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/CodexMcpElicitation.ts around lines 213-216:

With `acceptConfirmationChoices` enabled, ordinary Approve sends `{ persist: "Yes" }` for a string persistence field with enum ` ["Yes", "No"]`, silently granting an unseen persistent permission. The confirmation match checks only the option text and does not use `isMcpElicitationPersistenceField` for string fields; exclude persistence fields from this match or choose their negative option.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
);
if (chosenOption) {
content[key] = chosenOption.value;
Expand Down
Loading