| 1 | import type { ProviderApprovalDecision, ProviderApprovalOption } from "@t3tools/contracts"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | import type * as EffectCodexSchema from "effect-codex-app-server/schema"; |
| 4 | |
| 5 | const NullableMcpElicitationString = Schema.NullOr(Schema.String); |
| 6 | const 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 | }); |
| 33 | const 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 | }); |
| 51 | const McpElicitationForm = Schema.Struct({ |
| 52 | properties: Schema.optionalKey(Schema.Record(Schema.String, McpElicitationFormField)), |
| 53 | required: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))), |
| 54 | }); |
| 55 | const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata); |
| 56 | const isMcpElicitationForm = Schema.is(McpElicitationForm); |
| 57 | |
| 58 | type McpElicitationPersistenceDecision = Extract< |
| 59 | ProviderApprovalDecision, |
| 60 | "acceptForSession" | "acceptAlways" |
| 61 | >; |
| 62 | |
| 63 | function 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 | |
| 79 | function mcpElicitationFormFields(payload: EffectCodexSchema.McpServerElicitationRequestParams) { |
| 80 | if (payload.mode === "url" || !isMcpElicitationForm(payload.requestedSchema)) { |
| 81 | return undefined; |
| 82 | } |
| 83 | return payload.requestedSchema; |
| 84 | } |
| 85 | |
| 86 | function 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 | |
| 96 | function 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. */ |
| 109 | export 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. */ |
| 175 | export 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 | } |