| 32 | * 6. `await authProvider.close()` when done (success or permanent failure) |
| 33 | */ |
| 34 | export class McpOAuthClientProvider implements OAuthClientProvider { |
| 35 | // ── Static negative cache ──────────────────────────────────────────────── |
| 36 | // Remembers servers that returned no OAuth metadata so we can skip the |
| 37 | // discovery probe on subsequent connection attempts (reconnect, restart). |
| 38 | private static _nonOAuthCache = new Map<string, number>() // serverUrl → timestamp |
| 39 | private static NON_OAUTH_TTL_MS = 30 * 60 * 1000 // 30 minutes |
| 40 | |
| 41 | static isKnownNonOAuth(serverUrl: string): boolean { |
| 42 | const ts = McpOAuthClientProvider._nonOAuthCache.get(serverUrl) |
| 43 | if (ts === undefined) return false |
| 44 | if (Date.now() - ts > McpOAuthClientProvider.NON_OAUTH_TTL_MS) { |
| 45 | McpOAuthClientProvider._nonOAuthCache.delete(serverUrl) |
| 46 | return false |
| 47 | } |
| 48 | return true |
| 49 | } |
| 50 | |
| 51 | static markNonOAuth(serverUrl: string): void { |
| 52 | McpOAuthClientProvider._nonOAuthCache.set(serverUrl, Date.now()) |
| 53 | } |
| 54 | |
| 55 | static clearNonOAuthCache(serverUrl?: string): void { |
| 56 | if (serverUrl) { |
| 57 | McpOAuthClientProvider._nonOAuthCache.delete(serverUrl) |
| 58 | } else { |
| 59 | McpOAuthClientProvider._nonOAuthCache.clear() |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | // ── Instance fields ────────────────────────────────────────────────────── |
| 64 | private _codeVerifier?: string |
| 65 | // Client info is kept in-memory only (not persisted) to avoid stale registrations |
| 66 | // when the redirect URI port changes between sessions. |
| 67 | private _clientInfo?: OAuthClientInformationFull |
| 68 | private _closed = false |
| 69 | private _refreshPromise: Promise<OAuthTokens> | null = null |
| 70 | /** Stored by redirectToAuthorization(); opened on-demand via openBrowser(). */ |
| 71 | private _pendingAuthorizationUrl: URL | null = null |
| 72 | /** Deduplicates concurrent _ensureCallbackServer() calls. */ |
| 73 | private _ensureServerPromise: Promise<void> | null = null |
| 74 | |
| 75 | private constructor( |
| 76 | private readonly _serverUrl: string, |
| 77 | private readonly _secretStorage: SecretStorageService, |
| 78 | private _server: http.Server | null, |
| 79 | private _port: number, |
| 80 | private _authCodePromise: Promise<string> | null, |
| 81 | private _cancelCallbackServer: (() => void) | null, |
| 82 | private readonly _tokenEndpointAuthMethod: string, |
| 83 | private readonly _grantTypes: string[], |
| 84 | private readonly _scopes: string[], |
| 85 | private readonly _state: string, |
| 86 | private readonly _authServerMeta: Record<string, any> | null, |
| 87 | private readonly _resourceIndicator: string | null, |
| 88 | private readonly _clientName: string, |
| 89 | ) {} |
| 90 | |
| 91 | /** |
nothing calls this directly
no outgoing calls
no test coverage detected