fix(server): show Claude MCP elicitations on the approval card#21903

open1
main
GitHub fork: fix/claude-mcp-elicitation-approval
opened5d agobyCchrischriscris
View on GitHubTwo-way sync enabledPR last imported 4d ago
2 unresolved threads · Jump to file
Selected thread: apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:2673 — Fixed in cac644c5d7. The Claude path now drops `default` from every field before filling the form, so Approve only sends a one-time choice or a `false` persistence toggle. A required field that only a default could fill is declined with no card, and an optional one is left out.
0 / 3 viewed3 files changedLine totals unavailable

apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts · Earlier revision db9449db · right 2654–2673

Original diff
@@ -2617,6 +2623,56 @@ export const awaitClaudeApprovalDecision = Effect.fn("awaitClaudeApprovalDecisio
   return yield* Effect.raceFirst(Deferred.await(decision), cancellation);
 });
 
+// Web and mobile offer a session grant when options are omitted, and this
+// path only ever answers once.
+const CLAUDE_MCP_ELICITATION_OPTIONS: ReadonlyArray<ProviderApprovalOption> = [
+  { decision: "cancel", label: "Cancel" },
+  { decision: "decline", label: "Decline" },
+  { decision: "accept", label: "Approve" },
+];
+
+type ClaudeMcpElicitationContent = NonNullable<ElicitationResult["content"]>;
+
+const isClaudeMcpElicitationValue = (
+  value: unknown,
+): value is ClaudeMcpElicitationContent[string] =>
+  typeof value === "string" ||
+  typeof value === "number" ||
+  typeof value === "boolean" ||
+  (Array.isArray(value) && value.every((item) => typeof item === "string"));
+
+/**
+ * The answer an Approve click sends, or undefined when the card cannot answer
+ * the form. The card only collects consent, so the form is filled the way
+ * Codex fills it (approval choices and defaults) and anything left unfilled,
+ * URL mode included, is declined before a card is shown.
+ */
+export function claudeMcpElicitationAcceptance(
+  request: ElicitationRequest,
+): ElicitationResult | undefined {
+  if (request.mode === "url" || request.requestedSchema === undefined) return undefined;
+  const response = toMcpElicitationResponse(
+    { mode: "form", requestedSchema: request.requestedSchema },
+    "accept",
+  );
+  // The helper leaves content out when the schema is not a form it can read.
+  const filled = response.content;
+  if (
+    response.action !== "accept" ||
+    typeof filled !== "object" ||
+    filled === null ||
+    Array.isArray(filled)
+  ) {
+    return undefined;
+  }
+  const content: ClaudeMcpElicitationContent = {};
+  for (const [key, value] of Object.entries(filled)) {
+    if (!isClaudeMcpElicitationValue(value)) return undefined;
+    content[key] = value;
+  }
+  return { action: "accept", content };

apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts · right 2654–2673

C

Fixed in cac644c5d7. The Claude path now drops default from every field before filling the form, so Approve only sends a one-time choice or a false persistence toggle. A required field that only a default could fill is declined with no card, and an optional one is left out.