Skip to content

Commit 3fd7681

Browse files
committed
Remove handler from transport definition
1 parent bf9f63e commit 3fd7681

File tree

8 files changed

+168
-174
lines changed

8 files changed

+168
-174
lines changed

samples/AuthorizationExample/Program.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ public static async Task Main(string[] args)
3636
RedirectUris = new[]
3737
{
3838
$"http://{hostname}:{port}{callbackPath}"
39-
},
40-
41-
// Configure the authorize callback with the same hostname, port, and path
42-
AuthorizeCallback = SseClientTransport.CreateHttpListenerAuthorizeCallback(
39+
}, // Configure the authorize callback with the same hostname, port, and path
40+
AuthorizeCallback = AuthorizationService.CreateHttpListenerAuthorizeCallback(
4341
openBrowser: async (url) =>
4442
{
4543
Console.WriteLine($"Opening browser to authorize at: {url}");

src/ModelContextProtocol/Protocol/Auth/AuthorizationServerMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace ModelContextProtocol.Protocol.Auth;
55
/// <summary>
66
/// Represents OAuth 2.0 authorization server metadata as defined in RFC 8414.
77
/// </summary>
8-
internal class AuthorizationServerMetadata
8+
public class AuthorizationServerMetadata
99
{
1010
/// <summary>
1111
/// Gets or sets the authorization endpoint URL.

src/ModelContextProtocol/Protocol/Auth/AuthorizationService.cs

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace ModelContextProtocol.Protocol.Auth;
1111
/// <summary>
1212
/// Provides OAuth 2.0 authorization services for MCP clients.
1313
/// </summary>
14-
internal class AuthorizationService
14+
public class AuthorizationService
1515
{
1616
private static readonly HttpClient s_httpClient = new()
1717
{
@@ -448,9 +448,166 @@ private static Dictionary<string, string> ParseAuthHeaderParameters(string param
448448
break;
449449

450450
start = commaPos + 1;
451-
}
452-
}
451+
} }
453452

454453
return result;
455454
}
455+
456+
/// <summary>
457+
/// Creates an HTTP listener callback for handling OAuth 2.0 authorization code flow.
458+
/// </summary>
459+
/// <param name="openBrowser">A function that opens a browser with the given URL.</param>
460+
/// <param name="hostname">The hostname to listen on. Defaults to "localhost".</param>
461+
/// <param name="listenPort">The port to listen on. Defaults to 8888.</param>
462+
/// <param name="redirectPath">The redirect path for the HTTP listener. Defaults to "/callback".</param>
463+
/// <returns>
464+
/// A function that takes <see cref="ClientMetadata"/> and returns a task that resolves to a tuple containing
465+
/// the redirect URI and the authorization code.
466+
/// </returns>
467+
public static Func<ClientMetadata, Task<(string RedirectUri, string Code)>> CreateHttpListenerAuthorizeCallback(
468+
Func<string, Task> openBrowser,
469+
string hostname = "localhost",
470+
int listenPort = 8888,
471+
string redirectPath = "/callback")
472+
{
473+
return async (ClientMetadata clientMetadata) =>
474+
{
475+
string redirectUri = $"http://{hostname}:{listenPort}{redirectPath}";
476+
477+
foreach (var uri in clientMetadata.RedirectUris)
478+
{
479+
if (uri.StartsWith($"http://{hostname}", StringComparison.OrdinalIgnoreCase) &&
480+
Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
481+
{
482+
redirectUri = uri;
483+
listenPort = parsedUri.IsDefaultPort ? 80 : parsedUri.Port;
484+
redirectPath = parsedUri.AbsolutePath;
485+
break;
486+
}
487+
}
488+
489+
var authCodeTcs = new TaskCompletionSource<string>();
490+
// Ensure the path has a trailing slash for the HttpListener prefix
491+
string listenerPrefix = $"http://{hostname}:{listenPort}{redirectPath}";
492+
if (!listenerPrefix.EndsWith("/"))
493+
{
494+
listenerPrefix += "/";
495+
}
496+
497+
using var listener = new HttpListener();
498+
listener.Prefixes.Add(listenerPrefix);
499+
500+
// Start the listener BEFORE opening the browser
501+
try
502+
{
503+
listener.Start();
504+
}
505+
catch (HttpListenerException ex)
506+
{
507+
throw new McpException($"Failed to start HTTP listener on {listenerPrefix}: {ex.Message}", McpErrorCode.InvalidRequest);
508+
}
509+
510+
// Create a cancellation token source with a timeout
511+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
512+
513+
_ = Task.Run(async () =>
514+
{
515+
try
516+
{
517+
// GetContextAsync doesn't accept a cancellation token, so we need to handle cancellation manually
518+
var contextTask = listener.GetContextAsync();
519+
var completedTask = await Task.WhenAny(contextTask, Task.Delay(Timeout.Infinite, cts.Token));
520+
521+
if (completedTask == contextTask)
522+
{
523+
var context = await contextTask;
524+
var request = context.Request;
525+
var response = context.Response;
526+
527+
string? code = request.QueryString["code"];
528+
string? error = request.QueryString["error"];
529+
string html;
530+
string? resultCode = null;
531+
532+
if (!string.IsNullOrEmpty(error))
533+
{
534+
html = $"<html><body><h1>Authorization Failed</h1><p>Error: {WebUtility.HtmlEncode(error)}</p></body></html>";
535+
}
536+
else if (string.IsNullOrEmpty(code))
537+
{
538+
html = "<html><body><h1>Authorization Failed</h1><p>No authorization code received.</p></body></html>";
539+
}
540+
else
541+
{
542+
html = "<html><body><h1>Authorization Successful</h1><p>You may now close this window.</p></body></html>";
543+
resultCode = code;
544+
}
545+
546+
try
547+
{
548+
// Send response to browser
549+
byte[] buffer = Encoding.UTF8.GetBytes(html);
550+
response.ContentType = "text/html";
551+
response.ContentLength64 = buffer.Length;
552+
response.OutputStream.Write(buffer, 0, buffer.Length);
553+
554+
// IMPORTANT: Explicitly close the response to ensure it's fully sent
555+
response.Close();
556+
557+
// Now that we've finished processing the browser response,
558+
// we can safely signal completion or failure with the auth code
559+
if (resultCode != null)
560+
{
561+
authCodeTcs.TrySetResult(resultCode);
562+
}
563+
else if (!string.IsNullOrEmpty(error))
564+
{
565+
authCodeTcs.TrySetException(new McpException($"Authorization failed: {error}", McpErrorCode.InvalidRequest));
566+
}
567+
else
568+
{
569+
authCodeTcs.TrySetException(new McpException("No authorization code received", McpErrorCode.InvalidRequest));
570+
}
571+
}
572+
catch (Exception ex)
573+
{
574+
authCodeTcs.TrySetException(new McpException($"Error processing browser response: {ex.Message}", McpErrorCode.InvalidRequest));
575+
}
576+
}
577+
}
578+
catch (Exception ex)
579+
{
580+
authCodeTcs.TrySetException(ex);
581+
}
582+
});
583+
584+
// Now open the browser AFTER the listener is started
585+
if (!string.IsNullOrEmpty(clientMetadata.ClientUri))
586+
{
587+
await openBrowser(clientMetadata.ClientUri!);
588+
}
589+
else
590+
{
591+
// Stop the listener before throwing
592+
listener.Stop();
593+
throw new McpException("Client URI is missing in metadata.", McpErrorCode.InvalidRequest);
594+
}
595+
596+
try
597+
{
598+
// Use a timeout to avoid hanging indefinitely
599+
string authCode = await authCodeTcs.Task.WaitAsync(cts.Token);
600+
return (redirectUri, authCode);
601+
}
602+
catch (OperationCanceledException)
603+
{
604+
throw new McpException("Authorization timed out after 5 minutes.", McpErrorCode.InvalidRequest);
605+
}
606+
finally
607+
{
608+
// Ensure the listener is stopped when we're done
609+
listener.Stop();
610+
}
611+
};
612+
}
456613
}

src/ModelContextProtocol/Protocol/Auth/ClientRegistrationResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace ModelContextProtocol.Protocol.Auth;
55
/// <summary>
66
/// Represents the OAuth 2.0 client registration response as defined in RFC 7591.
77
/// </summary>
8-
internal class ClientRegistrationResponse
8+
public class ClientRegistrationResponse
99
{
1010
/// <summary>
1111
/// Gets or sets the OAuth 2.0 client identifier string.

src/ModelContextProtocol/Protocol/Auth/ResourceMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace ModelContextProtocol.Protocol.Auth;
55
/// <summary>
66
/// Represents the resource metadata from the WWW-Authenticate header in a 401 Unauthorized response.
77
/// </summary>
8-
internal class ResourceMetadata
8+
public class ResourceMetadata
99
{
1010
/// <summary>
1111
/// Gets or sets the resource identifier URI.

src/ModelContextProtocol/Protocol/Auth/TokenResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace ModelContextProtocol.Protocol.Auth;
55
/// <summary>
66
/// Represents the OAuth 2.0 token response as defined in RFC 6749.
77
/// </summary>
8-
internal class TokenResponse
8+
public class TokenResponse
99
{
1010
/// <summary>
1111
/// Gets or sets the access token issued by the authorization server.

0 commit comments

Comments
 (0)