1import type { ProviderApprovalDecision, ProviderApprovalOption } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3import type * as EffectCodexSchema from "effect-codex-app-server/schema";
4
5const NullableMcpElicitationString = Schema.NullOr(Schema.String);
6const McpElicitationMetadata = Schema.Struct({
7 app: Schema.optionalKey(NullableMcpElicitationString),
8 app_name: Schema.optionalKey(NullableMcpElicitationString),
9 appName: Schema.optionalKey(NullableMcpElicitationString),
10 connector_name: Schema.optionalKey(NullableMcpElicitationString),
11 connectorName: Schema.optionalKey(NullableMcpElicitationString),
12 allowPersistentApproval: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
13 persist: Schema.optionalKey(
14 Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)])),
15 ),
16 target: Schema.optionalKey(
17 Schema.NullOr(
18 Schema.Struct({
19 app: Schema.optionalKey(NullableMcpElicitationString),
20 name: Schema.optionalKey(NullableMcpElicitationString),
21 }),
22 ),
23 ),
24 tool_params: Schema.optionalKey(
25 Schema.NullOr(
26 Schema.Struct({
27 app: Schema.optionalKey(NullableMcpElicitationString),
28 app_name: Schema.optionalKey(NullableMcpElicitationString),
29 }),
30 ),
31 ),
32});
33const McpElicitationFormField = Schema.Struct({
34 type: Schema.optionalKey(NullableMcpElicitationString),
35 title: Schema.optionalKey(NullableMcpElicitationString),
36 description: Schema.optionalKey(NullableMcpElicitationString),
37 default: Schema.optionalKey(Schema.Json),
38 enum: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
39 enumNames: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
40 oneOf: Schema.optionalKey(
41 Schema.NullOr(
42 Schema.Array(
43 Schema.Struct({
44 const: Schema.String,
45 title: Schema.optionalKey(NullableMcpElicitationString),
46 }),
47 ),
48 ),
49 ),
50});
51const McpElicitationForm = Schema.Struct({
52 properties: Schema.optionalKey(Schema.Record(Schema.String, McpElicitationFormField)),
53 required: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
54});
55const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata);
56const isMcpElicitationForm = Schema.is(McpElicitationForm);
57
58type McpElicitationPersistenceDecision = Extract<
59 ProviderApprovalDecision,
60 "acceptForSession" | "acceptAlways"
61>;
62
63function mcpElicitationPersistenceDecision(
64 value: string,
65): McpElicitationPersistenceDecision | null {
66 const normalized = value.toLowerCase();
67 if (normalized.includes("session")) return "acceptForSession";
68 if (
69 normalized.includes("always") ||
70 normalized.includes("permanent") ||
71 normalized.includes("forever") ||
72 normalized.includes("persistent")
73 ) {
74 return "acceptAlways";
75 }
76 return null;
77}
78
79function mcpElicitationFormFields(payload: EffectCodexSchema.McpServerElicitationRequestParams) {
80 if (payload.mode === "url" || !isMcpElicitationForm(payload.requestedSchema)) {
81 return undefined;
82 }
83 return payload.requestedSchema;
84}
85
86function mcpElicitationFieldOptions(field: typeof McpElicitationFormField.Type) {
87 if (field.oneOf) {
88 return field.oneOf.map((option) => ({ value: option.const, label: option.title }));
89 }
90 return (field.enum ?? []).map((value, index) => ({
91 value,
92 label: field.enumNames?.[index],
93 }));
94}
95
96function isMcpElicitationPersistenceField(
97 key: string,
98 field: typeof McpElicitationFormField.Type,
99): boolean {
100 return (
101 mcpElicitationPersistenceDecision(key) !== null ||
102 key.toLowerCase() === "persist" ||
103 mcpElicitationPersistenceDecision(field.title ?? "") !== null ||
104 mcpElicitationPersistenceDecision(field.description ?? "") !== null
105 );
106}
107
108/** Returns the app and approval choices advertised by an MCP elicitation. */
109export function describeMcpElicitation(
110 payload: EffectCodexSchema.McpServerElicitationRequestParams,
111): { readonly appName: string; readonly options: ReadonlyArray<ProviderApprovalOption> } {
112 const metadata = isMcpElicitationMetadata(payload._meta) ? payload._meta : undefined;
113 const appName =
114 metadata?.app_name ??
115 metadata?.appName ??
116 metadata?.app ??
117 metadata?.target?.app ??
118 metadata?.target?.name ??
119 metadata?.tool_params?.app_name ??
120 metadata?.tool_params?.app ??
121 payload.message.match(/^Allow ChatGPT to use (.+?)\?$/i)?.[1] ??
122 metadata?.connector_name ??
123 metadata?.connectorName ??
124 payload.serverName;
125 const persistenceOptions = new Map<McpElicitationPersistenceDecision, string>();
126 const persist = metadata?.persist;
127 for (const value of typeof persist === "string" ? [persist] : (persist ?? [])) {
128 const decision = mcpElicitationPersistenceDecision(value);
129 if (decision) persistenceOptions.set(decision, "");
130 }
131 if (metadata?.allowPersistentApproval) {
132 persistenceOptions.set("acceptAlways", "");
133 }
134
135 const form = mcpElicitationFormFields(payload);
136 for (const [key, field] of Object.entries(form?.properties ?? {})) {
137 for (const option of mcpElicitationFieldOptions(field)) {
138 const decision = mcpElicitationPersistenceDecision(option.value);
139 if (decision) persistenceOptions.set(decision, option.label ?? "");
140 }
141 if (field.type === "boolean" && isMcpElicitationPersistenceField(key, field)) {
142 persistenceOptions.set("acceptAlways", field.title ?? "");
143 }
144 }
145
146 return {
147 appName,
148 options: [
149 { decision: "cancel", label: "Cancel" },
150 { decision: "decline", label: "Decline" },
151 ...(persistenceOptions.has("acceptForSession") &&
152 toMcpElicitationResponse(payload, "acceptForSession").action === "accept"
153 ? [
154 {
155 decision: "acceptForSession" as const,
156 label: persistenceOptions.get("acceptForSession") || "Always allow this session",
157 },
158 ]
159 : []),
160 ...(persistenceOptions.has("acceptAlways") &&
161 toMcpElicitationResponse(payload, "acceptAlways").action === "accept"
162 ? [
163 {
164 decision: "acceptAlways" as const,
165 label: persistenceOptions.get("acceptAlways") || "Always allow",
166 },
167 ]
168 : []),
169 { decision: "accept", label: "Approve" },
170 ],
171 };
172}
173
174/** Converts a T3 approval decision into the MCP elicitation wire response. */
175export function toMcpElicitationResponse(
176 payload: EffectCodexSchema.McpServerElicitationRequestParams,
177 decision: ProviderApprovalDecision,
178): EffectCodexSchema.McpServerElicitationRequestResponse {
179 if (decision === "decline" || decision === "cancel") {
180 return { action: decision };
181 }
182
183 if (payload.mode === "url") {
184 return { action: "decline" };
185 }
186
187 const persist =
188 decision === "acceptForSession"
189 ? "session"
190 : decision === "acceptAlways"
191 ? "always"
192 : undefined;
193 const form = mcpElicitationFormFields(payload);
194 const content: Record<string, Schema.Json> = {};
195
196 for (const [key, field] of Object.entries(form?.properties ?? {})) {
197 const options = mcpElicitationFieldOptions(field);
198 const chosenOption = options.find((option) =>
199 persist
200 ? mcpElicitationPersistenceDecision(option.value) === decision
201 : /once|accept|approve|allow/i.test(option.value) &&
202 mcpElicitationPersistenceDecision(option.value) === null,
203 );
204 if (chosenOption) {
205 content[key] = chosenOption.value;
206 } else if (field.type === "boolean" && isMcpElicitationPersistenceField(key, field)) {
207 content[key] = decision === "acceptAlways";
208 } else if (field.default !== undefined && field.default !== null) {
209 content[key] = field.default;
210 }
211 }
212
213 if (form?.required?.some((key) => !Object.hasOwn(content, key))) {
214 return { action: "decline" };
215 }
216
217 return {
218 action: "accept",
219 ...(persist ? { _meta: { persist } } : {}),
220 ...(form ? { content } : {}),
221 };
222}