()
| 679 | |
| 680 | // Start the OAuth flow |
| 681 | async authenticate(): Promise<{ tokens: OAuthTokens; clientId: string }> { |
| 682 | this.callbacks.onStatus('Fetching OAuth server configuration...'); |
| 683 | |
| 684 | // Get server metadata |
| 685 | let metadata; |
| 686 | try { |
| 687 | metadata = await this.getServerMetadata(); |
| 688 | this.callbacks.onStatus(`Found OAuth endpoints at ${this.config.mcpBaseUrl}`); |
| 689 | } catch (error) { |
| 690 | const msg = error instanceof Error ? error.message : 'Unknown error'; |
| 691 | this.callbacks.onStatus(`Failed to get OAuth metadata: ${msg}`); |
| 692 | throw error; |
| 693 | } |
| 694 | |
| 695 | // Register client if endpoint available |
| 696 | let clientId: string; |
| 697 | if (metadata.registration_endpoint) { |
| 698 | // Try primary client name first, fall back to alternative if rejected |
| 699 | this.callbacks.onStatus(`Registering client as '${CLIENT_NAME}'...`); |
| 700 | try { |
| 701 | const client = await this.registerClient(metadata.registration_endpoint, CLIENT_NAME); |
| 702 | clientId = client.client_id; |
| 703 | this.callbacks.onStatus(`Registered as client: ${clientId}`); |
| 704 | } catch (error) { |
| 705 | // Try fallback client name (some servers have allowlists) |
| 706 | this.callbacks.onStatus(`Registration as '${CLIENT_NAME}' failed, trying '${FALLBACK_CLIENT_NAME}'...`); |
| 707 | try { |
| 708 | const client = await this.registerClient(metadata.registration_endpoint, FALLBACK_CLIENT_NAME); |
| 709 | clientId = client.client_id; |
| 710 | this.callbacks.onStatus(`Registered as client: ${clientId}`); |
| 711 | } catch (fallbackError) { |
| 712 | const msg = fallbackError instanceof Error ? fallbackError.message : 'Unknown error'; |
| 713 | this.callbacks.onStatus(`Client registration failed: ${msg}`); |
| 714 | throw fallbackError; |
| 715 | } |
| 716 | } |
| 717 | } else { |
| 718 | // Use a default client ID for public clients |
| 719 | clientId = 'craft-agent'; |
| 720 | this.callbacks.onStatus(`Using default client ID: ${clientId}`); |
| 721 | } |
| 722 | |
| 723 | // Generate PKCE and state |
| 724 | const pkce = generatePKCE(); |
| 725 | const state = generateState(); |
| 726 | const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; |
| 727 | this.callbacks.onStatus('Generated PKCE challenge and state'); |
| 728 | |
| 729 | // Build authorization URL |
| 730 | const authUrl = new URL(metadata.authorization_endpoint); |
| 731 | authUrl.searchParams.set('response_type', 'code'); |
| 732 | authUrl.searchParams.set('client_id', clientId); |
| 733 | authUrl.searchParams.set('redirect_uri', redirectUri); |
| 734 | authUrl.searchParams.set('state', state); |
| 735 | authUrl.searchParams.set('code_challenge', pkce.challenge); |
| 736 | authUrl.searchParams.set('code_challenge_method', 'S256'); |
| 737 | |
| 738 | // Start local server to receive callback |
no test coverage detected