(
serverName: string,
serverConfig: McpSSEServerConfig | McpHTTPServerConfig,
onAuthorizationUrl: (url: string) => void,
abortSignal?: AbortSignal,
options?: {
skipBrowserOpen?: boolean
onWaitingForCallback?: (submit: (callbackUrl: string) => void) => void
},
)
| 845 | } |
| 846 | |
| 847 | export async function performMCPOAuthFlow( |
| 848 | serverName: string, |
| 849 | serverConfig: McpSSEServerConfig | McpHTTPServerConfig, |
| 850 | onAuthorizationUrl: (url: string) => void, |
| 851 | abortSignal?: AbortSignal, |
| 852 | options?: { |
| 853 | skipBrowserOpen?: boolean |
| 854 | onWaitingForCallback?: (submit: (callbackUrl: string) => void) => void |
| 855 | }, |
| 856 | ): Promise<void> { |
| 857 | // XAA (SEP-990): if configured, bypass the per-server consent dance. |
| 858 | // If the IdP id_token isn't cached, this pops the browser once at the IdP |
| 859 | // (shared across all XAA servers for that issuer). Subsequent servers hit |
| 860 | // the cache and are silent. Tokens land in the same keychain slot, so the |
| 861 | // rest of CC's transport wiring (ClaudeAuthProvider.tokens() in client.ts) |
| 862 | // works unchanged. |
| 863 | // |
| 864 | // No silent fallback: if `oauth.xaa` is set, XAA is the only path. We |
| 865 | // never fall through to the consent flow — that would be surprising (the |
| 866 | // user explicitly asked for XAA) and security-relevant (consent flow may |
| 867 | // have a different trust/scope posture than the org's IdP policy). |
| 868 | // |
| 869 | // Servers with `oauth.xaa` but CLAUDE_CODE_ENABLE_XAA unset hard-fail with |
| 870 | // actionable copy rather than silently degrade to consent. |
| 871 | if (serverConfig.oauth?.xaa) { |
| 872 | if (!isXaaEnabled()) { |
| 873 | throw new Error( |
| 874 | `XAA is not enabled (set CLAUDE_CODE_ENABLE_XAA=1). Remove 'oauth.xaa' from server '${serverName}' to use the standard consent flow.`, |
| 875 | ) |
| 876 | } |
| 877 | logEvent('tengu_mcp_oauth_flow_start', { |
| 878 | isOAuthFlow: true, |
| 879 | authMethod: |
| 880 | 'xaa' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 881 | transportType: |
| 882 | serverConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 883 | ...(getLoggingSafeMcpBaseUrl(serverConfig) |
| 884 | ? { |
| 885 | mcpServerBaseUrl: getLoggingSafeMcpBaseUrl( |
| 886 | serverConfig, |
| 887 | ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 888 | } |
| 889 | : {}), |
| 890 | }) |
| 891 | // performMCPXaaAuth logs its own success/failure events (with |
| 892 | // idTokenCacheHit + xaaFailureStage). |
| 893 | await performMCPXaaAuth( |
| 894 | serverName, |
| 895 | serverConfig, |
| 896 | onAuthorizationUrl, |
| 897 | abortSignal, |
| 898 | options?.skipBrowserOpen, |
| 899 | ) |
| 900 | return |
| 901 | } |
| 902 | |
| 903 | // Check for cached step-up scope and resource metadata URL before clearing |
| 904 | // tokens. The transport-attached auth provider persists scope when it receives |
no test coverage detected