88
99namespace ModelContextProtocol . Authentication ;
1010
11+ /// <summary>
12+ /// Represents a method that handles the OAuth authorization URL and returns the authorization code.
13+ /// </summary>
14+ /// <param name="authorizationUrl">The authorization URL that the user needs to visit.</param>
15+ /// <param name="redirectUri">The redirect URI where the authorization code will be sent.</param>
16+ /// <param name="cancellationToken">The cancellation token.</param>
17+ /// <returns>A task that represents the asynchronous operation. The task result contains the authorization code if successful, or null if the operation failed or was cancelled.</returns>
18+ /// <remarks>
19+ /// <para>
20+ /// This delegate provides SDK consumers with full control over how the OAuth authorization flow is handled.
21+ /// Implementers can choose to:
22+ /// </para>
23+ /// <list type="bullet">
24+ /// <item><description>Start a local HTTP server and open a browser (default behavior)</description></item>
25+ /// <item><description>Display the authorization URL to the user for manual handling</description></item>
26+ /// <item><description>Integrate with a custom UI or authentication flow</description></item>
27+ /// <item><description>Use a different redirect mechanism altogether</description></item>
28+ /// </list>
29+ /// <para>
30+ /// The implementation should handle user interaction to visit the authorization URL and extract
31+ /// the authorization code from the callback. The authorization code is typically provided as
32+ /// a query parameter in the redirect URI callback.
33+ /// </para>
34+ /// </remarks>
35+ public delegate Task < string ? > AuthorizationUrlHandler ( Uri authorizationUrl , Uri redirectUri , CancellationToken cancellationToken ) ;
36+
1137/// <summary>
1238/// A generic implementation of an OAuth authorization provider for MCP. This does not do any advanced token
1339/// protection or caching - it acquires a token and server metadata and holds it in memory.
@@ -27,15 +53,14 @@ public class GenericOAuthProvider : IMcpCredentialProvider
2753 private readonly HttpClient _httpClient ; private readonly AuthorizationHelpers _authorizationHelpers ;
2854 private readonly ILogger _logger ;
2955 private readonly Func < IReadOnlyList < Uri > , Uri ? > _authServerSelector ;
56+ private readonly AuthorizationUrlHandler _authorizationUrlHandler ;
3057
3158 // Lazy-initialized shared HttpClient for when no client is provided
3259 private static readonly Lazy < HttpClient > _defaultHttpClient = new ( ( ) => new HttpClient ( ) ) ;
3360
3461 private static readonly JsonSerializerOptions _jsonOptions = new ( ) { PropertyNameCaseInsensitive = true } ;
3562 private TokenContainer ? _token ;
36- private AuthorizationServerMetadata ? _authServerMetadata ;
37-
38- /// <summary>
63+ private AuthorizationServerMetadata ? _authServerMetadata ; /// <summary>
3964 /// Initializes a new instance of the <see cref="GenericOAuthProvider"/> class.
4065 /// </summary>
4166 /// <param name="serverUrl">The MCP server URL.</param>
@@ -54,7 +79,34 @@ public GenericOAuthProvider(
5479 string clientSecret = "" ,
5580 Uri ? redirectUri = null ,
5681 IEnumerable < string > ? scopes = null ,
57- ILogger < GenericOAuthProvider > ? logger = null ) : this ( serverUrl , httpClient , authorizationHelpers , clientId , clientSecret , redirectUri , scopes , logger , null )
82+ ILogger < GenericOAuthProvider > ? logger = null )
83+ : this ( serverUrl , httpClient , authorizationHelpers , clientId , clientSecret , redirectUri , scopes , logger , null , null )
84+ {
85+ }
86+
87+ /// <summary>
88+ /// Initializes a new instance of the <see cref="GenericOAuthProvider"/> class with a custom authorization URL handler.
89+ /// </summary>
90+ /// <param name="serverUrl">The MCP server URL.</param>
91+ /// <param name="httpClient">The HTTP client to use for OAuth requests. If null, a default HttpClient will be used.</param>
92+ /// <param name="authorizationHelpers">The authorization helpers.</param>
93+ /// <param name="clientId">OAuth client ID.</param>
94+ /// <param name="clientSecret">OAuth client secret.</param>
95+ /// <param name="redirectUri">OAuth redirect URI.</param>
96+ /// <param name="scopes">OAuth scopes.</param>
97+ /// <param name="logger">The logger instance. If null, a NullLogger will be used.</param>
98+ /// <param name="authorizationUrlHandler">Custom handler for processing the OAuth authorization URL. If null, uses the default HTTP listener approach.</param>
99+ public GenericOAuthProvider (
100+ Uri serverUrl ,
101+ HttpClient ? httpClient ,
102+ AuthorizationHelpers ? authorizationHelpers ,
103+ string clientId ,
104+ string clientSecret ,
105+ Uri ? redirectUri ,
106+ IEnumerable < string > ? scopes ,
107+ ILogger < GenericOAuthProvider > ? logger ,
108+ AuthorizationUrlHandler ? authorizationUrlHandler )
109+ : this ( serverUrl , httpClient , authorizationHelpers , clientId , clientSecret , redirectUri , scopes , logger , null , authorizationUrlHandler )
58110 {
59111 } /// <summary>
60112 /// Initializes a new instance of the <see cref="GenericOAuthProvider"/> class with explicit authorization server selection.
@@ -68,6 +120,7 @@ public GenericOAuthProvider(
68120 /// <param name="scopes">OAuth scopes.</param>
69121 /// <param name="logger">The logger instance. If null, a NullLogger will be used.</param>
70122 /// <param name="authServerSelector">Function to select which authorization server to use from available servers. If null, uses default selection strategy.</param>
123+ /// <param name="authorizationUrlHandler">Custom handler for processing the OAuth authorization URL. If null, uses the default HTTP listener approach.</param>
71124 /// <exception cref="ArgumentNullException">Thrown when serverUrl is null.</exception>
72125 public GenericOAuthProvider (
73126 Uri serverUrl ,
@@ -78,7 +131,8 @@ public GenericOAuthProvider(
78131 Uri ? redirectUri ,
79132 IEnumerable < string > ? scopes ,
80133 ILogger < GenericOAuthProvider > ? logger ,
81- Func < IReadOnlyList < Uri > , Uri ? > ? authServerSelector )
134+ Func < IReadOnlyList < Uri > , Uri ? > ? authServerSelector ,
135+ AuthorizationUrlHandler ? authorizationUrlHandler )
82136 {
83137 if ( serverUrl == null ) throw new ArgumentNullException ( nameof ( serverUrl ) ) ;
84138
@@ -94,17 +148,35 @@ public GenericOAuthProvider(
94148
95149 // Set up authorization server selection strategy
96150 _authServerSelector = authServerSelector ?? DefaultAuthServerSelector ;
97- }
98-
99- /// <summary>
151+
152+ // Set up authorization URL handler (use default if not provided)
153+ _authorizationUrlHandler = authorizationUrlHandler ?? DefaultAuthorizationUrlHandler ;
154+ } /// <summary>
100155 /// Default authorization server selection strategy that selects the first available server.
101156 /// </summary>
102157 /// <param name="availableServers">List of available authorization servers.</param>
103- /// <returns>The selected authorization server, or null if none are available.</returns>
158+ /// <returns>The selected authorization server, or null if none are available.</returns>
104159 private static Uri ? DefaultAuthServerSelector ( IReadOnlyList < Uri > availableServers )
105160 {
106161 return availableServers . FirstOrDefault ( ) ;
107162 }
163+
164+ /// <summary>
165+ /// Default authorization URL handler that displays the URL to the user for manual input.
166+ /// </summary>
167+ /// <param name="authorizationUrl">The authorization URL to handle.</param>
168+ /// <param name="redirectUri">The redirect URI where the authorization code will be sent.</param>
169+ /// <param name="cancellationToken">The cancellation token.</param>
170+ /// <returns>The authorization code entered by the user, or null if none was provided.</returns>
171+ private Task < string ? > DefaultAuthorizationUrlHandler ( Uri authorizationUrl , Uri redirectUri , CancellationToken cancellationToken )
172+ {
173+ Console . WriteLine ( $ "Please open the following URL in your browser to authorize the application:") ;
174+ Console . WriteLine ( $ "{ authorizationUrl } ") ;
175+ Console . WriteLine ( ) ;
176+ Console . Write ( "Enter the authorization code from the redirect URL: " ) ;
177+ var authorizationCode = Console . ReadLine ( ) ;
178+ return Task . FromResult < string ? > ( authorizationCode ) ;
179+ }
108180
109181 /// <inheritdoc />
110182 public IEnumerable < string > SupportedSchemes => new [ ] { BearerScheme } ;
@@ -371,56 +443,9 @@ private Uri BuildAuthorizationUrl(AuthorizationServerMetadata authServerMetadata
371443 } ;
372444 return uriBuilder . Uri ;
373445 }
374-
375- private async Task < string ? > GetAuthorizationCodeAsync ( Uri authorizationUrl , CancellationToken cancellationToken )
446+ private async Task < string ? > GetAuthorizationCodeAsync ( Uri authorizationUrl , CancellationToken cancellationToken )
376447 {
377- var listenerPrefix = _redirectUri . GetLeftPart ( UriPartial . Authority ) ;
378- if ( ! listenerPrefix . EndsWith ( "/" ) ) listenerPrefix += "/" ;
379-
380- using var listener = new System . Net . HttpListener ( ) ;
381- listener . Prefixes . Add ( listenerPrefix ) ;
382-
383- try
384- {
385- listener . Start ( ) ;
386-
387- OpenBrowser ( authorizationUrl ) ;
388-
389- var context = await listener . GetContextAsync ( ) ;
390- var query = HttpUtility . ParseQueryString ( context . Request . Url ? . Query ?? string . Empty ) ;
391- var code = query [ "code" ] ;
392- var error = query [ "error" ] ;
393-
394- string responseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>" ;
395- byte [ ] buffer = Encoding . UTF8 . GetBytes ( responseHtml ) ;
396- context . Response . ContentLength64 = buffer . Length ;
397- context . Response . ContentType = "text/html" ;
398- context . Response . OutputStream . Write ( buffer , 0 , buffer . Length ) ;
399- context . Response . Close ( ) ;
400-
401- if ( ! string . IsNullOrEmpty ( error ) )
402- {
403- _logger . LogError ( "Auth error: {Error}" , error ) ;
404- return null ;
405- }
406-
407- if ( string . IsNullOrEmpty ( code ) )
408- {
409- _logger . LogError ( "No authorization code received" ) ;
410- return null ;
411- }
412-
413- return code ;
414- }
415- catch ( Exception ex )
416- {
417- _logger . LogError ( ex , "Error getting auth code" ) ;
418- return null ;
419- }
420- finally
421- {
422- if ( listener . IsListening ) listener . Stop ( ) ;
423- }
448+ return await _authorizationUrlHandler ( authorizationUrl , _redirectUri , cancellationToken ) ;
424449 }
425450
426451 private async Task < TokenContainer ? > ExchangeCodeForTokenAsync (
0 commit comments