();
+ // Ensure the path has a trailing slash for the HttpListener prefix
+ string listenerPrefix = $"http://{hostname}:{listenPort}{redirectPath}";
+ if (!listenerPrefix.EndsWith("/"))
+ {
+ listenerPrefix += "/";
+ }
+
+ using var listener = new HttpListener();
+ listener.Prefixes.Add(listenerPrefix);
+
+ // Start the listener BEFORE opening the browser
+ try
+ {
+ listener.Start();
+ }
+ catch (HttpListenerException ex)
+ {
+ throw new McpException($"Failed to start HTTP listener on {listenerPrefix}: {ex.Message}", McpErrorCode.InvalidRequest);
+ }
+
+ // Create a cancellation token source with a timeout
+ using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ // GetContextAsync doesn't accept a cancellation token, so we need to handle cancellation manually
+ var contextTask = listener.GetContextAsync();
+ var completedTask = await Task.WhenAny(contextTask, Task.Delay(Timeout.Infinite, cts.Token));
+
+ if (completedTask == contextTask)
+ {
+ var context = await contextTask;
+ var request = context.Request;
+ var response = context.Response;
+
+ string? code = request.QueryString["code"];
+ string? error = request.QueryString["error"];
+ string html;
+ string? resultCode = null;
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ html = $"Authorization Failed
Error: {WebUtility.HtmlEncode(error)}
";
+ }
+ else if (string.IsNullOrEmpty(code))
+ {
+ html = "Authorization Failed
No authorization code received.
";
+ }
+ else
+ {
+ html = "Authorization Successful
You may now close this window.
";
+ resultCode = code;
+ }
+
+ try
+ {
+ // Send response to browser
+ byte[] buffer = Encoding.UTF8.GetBytes(html);
+ response.ContentType = "text/html";
+ response.ContentLength64 = buffer.Length;
+ response.OutputStream.Write(buffer, 0, buffer.Length);
+
+ // IMPORTANT: Explicitly close the response to ensure it's fully sent
+ response.Close();
+
+ // Now that we've finished processing the browser response,
+ // we can safely signal completion or failure with the auth code
+ if (resultCode != null)
+ {
+ authCodeTcs.TrySetResult(resultCode);
+ }
+ else if (!string.IsNullOrEmpty(error))
+ {
+ authCodeTcs.TrySetException(new McpException($"Authorization failed: {error}", McpErrorCode.InvalidRequest));
+ }
+ else
+ {
+ authCodeTcs.TrySetException(new McpException("No authorization code received", McpErrorCode.InvalidRequest));
+ }
+ }
+ catch (Exception ex)
+ {
+ authCodeTcs.TrySetException(new McpException($"Error processing browser response: {ex.Message}", McpErrorCode.InvalidRequest));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ authCodeTcs.TrySetException(ex);
+ }
+ });
+
+ // Now open the browser AFTER the listener is started
+ if (!string.IsNullOrEmpty(clientMetadata.ClientUri))
+ {
+ await openBrowser(clientMetadata.ClientUri!);
+ }
+ else
+ {
+ // Stop the listener before throwing
+ listener.Stop();
+ throw new McpException("Client URI is missing in metadata.", McpErrorCode.InvalidRequest);
+ }
+
+ try
+ {
+ // Use a timeout to avoid hanging indefinitely
+ string authCode = await authCodeTcs.Task.WaitAsync(cts.Token);
+ return (redirectUri, authCode);
+ }
+ catch (OperationCanceledException)
+ {
+ throw new McpException("Authorization timed out after 5 minutes.", McpErrorCode.InvalidRequest);
+ }
+ finally
+ {
+ // Ensure the listener is stopped when we're done
+ listener.Stop();
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/ClientMetadata.cs b/src/ModelContextProtocol/Protocol/Auth/ClientMetadata.cs
new file mode 100644
index 00000000..d650c5a2
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/ClientMetadata.cs
@@ -0,0 +1,99 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Represents the OAuth 2.0 client registration metadata as defined in RFC 7591.
+///
+public class ClientMetadata
+{
+ ///
+ /// Gets or sets the array of redirection URI strings for use in redirect-based flows.
+ ///
+ [JsonPropertyName("redirect_uris")]
+ public required string[] RedirectUris { get; set; }
+
+ ///
+ /// Gets or sets the requested authentication method for the token endpoint.
+ ///
+ [JsonPropertyName("token_endpoint_auth_method")]
+ public string? TokenEndpointAuthMethod { get; set; } = "client_secret_basic";
+
+ ///
+ /// Gets or sets the array of OAuth 2.0 grant type strings that the client can use at the token endpoint.
+ ///
+ [JsonPropertyName("grant_types")]
+ public string[]? GrantTypes { get; set; } = ["authorization_code", "refresh_token"];
+
+ ///
+ /// Gets or sets the array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint.
+ ///
+ [JsonPropertyName("response_types")]
+ public string[]? ResponseTypes { get; set; } = ["code"];
+
+ ///
+ /// Gets or sets the human-readable string name of the client.
+ ///
+ [JsonPropertyName("client_name")]
+ public string? ClientName { get; set; }
+
+ ///
+ /// Gets or sets the URL string of a web page providing information about the client.
+ ///
+ [JsonPropertyName("client_uri")]
+ public string? ClientUri { get; set; }
+
+ ///
+ /// Gets or sets the URL string that references a logo for the client.
+ ///
+ [JsonPropertyName("logo_uri")]
+ public string? LogoUri { get; set; }
+
+ ///
+ /// Gets or sets the string containing a space-separated list of scope values that the client can use.
+ ///
+ [JsonPropertyName("scope")]
+ public string? Scope { get; set; }
+
+ ///
+ /// Gets or sets the array of strings representing ways to contact people responsible for this client.
+ ///
+ [JsonPropertyName("contacts")]
+ public string[]? Contacts { get; set; }
+
+ ///
+ /// Gets or sets the URL string that points to a human-readable terms of service document.
+ ///
+ [JsonPropertyName("tos_uri")]
+ public string? TosUri { get; set; }
+
+ ///
+ /// Gets or sets the URL string that points to a human-readable privacy policy document.
+ ///
+ [JsonPropertyName("policy_uri")]
+ public string? PolicyUri { get; set; }
+
+ ///
+ /// Gets or sets the URL string referencing the client's JSON Web Key Set document.
+ ///
+ [JsonPropertyName("jwks_uri")]
+ public string? JwksUri { get; set; }
+
+ ///
+ /// Gets or sets the client's JSON Web Key Set document value.
+ ///
+ [JsonPropertyName("jwks")]
+ public object? Jwks { get; set; }
+
+ ///
+ /// Gets or sets a unique identifier string assigned by the client developer or software publisher.
+ ///
+ [JsonPropertyName("software_id")]
+ public string? SoftwareId { get; set; }
+
+ ///
+ /// Gets or sets the version identifier string for the client software.
+ ///
+ [JsonPropertyName("software_version")]
+ public string? SoftwareVersion { get; set; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/ClientRegistration.cs b/src/ModelContextProtocol/Protocol/Auth/ClientRegistration.cs
new file mode 100644
index 00000000..d4e66b98
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/ClientRegistration.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Represents the OAuth 2.0 client registration response as defined in RFC 7591.
+///
+public class ClientRegistration
+{
+ ///
+ /// Gets or sets the OAuth 2.0 client identifier string.
+ ///
+ [JsonPropertyName("client_id")]
+ public required string ClientId { get; set; }
+
+ ///
+ /// Gets or sets the OAuth 2.0 client secret string.
+ ///
+ [JsonPropertyName("client_secret")]
+ public string? ClientSecret { get; set; }
+
+ ///
+ /// Gets or sets the time at which the client identifier was issued.
+ ///
+ [JsonPropertyName("client_id_issued_at")]
+ public long? ClientIdIssuedAt { get; set; }
+
+ ///
+ /// Gets or sets the time at which the client secret will expire or 0 if it will not expire.
+ ///
+ [JsonPropertyName("client_secret_expires_at")]
+ public long? ClientSecretExpiresAt { get; set; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/DefaultAuthorizationHandler.cs b/src/ModelContextProtocol/Protocol/Auth/DefaultAuthorizationHandler.cs
new file mode 100644
index 00000000..c10286f3
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/DefaultAuthorizationHandler.cs
@@ -0,0 +1,295 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using ModelContextProtocol.Utils;
+using System.Net;
+using System.Net.Http.Headers;
+
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Provides authorization handling for MCP clients.
+///
+internal class DefaultAuthorizationHandler : IAuthorizationHandler
+{
+ private readonly ILogger _logger;
+ private readonly SynchronizedValue _authContext = new(new AuthorizationContext());
+ private readonly Func>? _authorizeCallback;
+ private readonly string? _clientId;
+ private readonly string? _clientSecret;
+ private readonly ICollection? _redirectUris;
+ private readonly ICollection? _scopes;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger factory.
+ /// The authorization options.
+ public DefaultAuthorizationHandler(ILoggerFactory? loggerFactory = null, AuthorizationOptions? options = null)
+ {
+ _logger = loggerFactory != null
+ ? loggerFactory.CreateLogger()
+ : NullLogger.Instance;
+
+ if (options != null)
+ {
+ _authorizeCallback = options.AuthorizeCallback;
+ _clientId = options.ClientId;
+ _clientSecret = options.ClientSecret;
+ _redirectUris = options.RedirectUris;
+ _scopes = options.Scopes;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger factory.
+ /// A callback function that handles the authorization code flow.
+ public DefaultAuthorizationHandler(ILoggerFactory? loggerFactory = null, Func>? authorizeCallback = null)
+ : this(loggerFactory, new AuthorizationOptions { AuthorizeCallback = authorizeCallback })
+ {
+ }
+
+ ///
+ public async Task AuthenticateRequestAsync(HttpRequestMessage request)
+ {
+ // Try to get a valid token, refreshing if necessary
+ var token = await GetValidTokenAsync();
+
+ if (!string.IsNullOrEmpty(token))
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ }
+ }
+
+ ///
+ public async Task HandleUnauthorizedResponseAsync(HttpResponseMessage response, Uri serverUri)
+ {
+ if (response.StatusCode != HttpStatusCode.Unauthorized)
+ {
+ return false;
+ }
+
+ _logger.LogDebug("Received 401 Unauthorized response from {ServerUri}", serverUri);
+
+ using var authContext = await _authContext.LockAsync();
+
+ // If we already have a valid token, it might be that the token was just revoked
+ // or has other issues - we need to clear our state and retry the authorization flow
+ if (authContext.Value.HasValidToken)
+ {
+ _logger.LogWarning("Server rejected our authentication token. Clearing authentication state and reauthorizing.");
+ authContext.Value = new AuthorizationContext();
+ }
+
+ // Try to get resource metadata from the response
+ var resourceMetadata = await AuthorizationService.GetResourceMetadataFromResponseAsync(response);
+ if (resourceMetadata == null)
+ {
+ _logger.LogWarning("Failed to extract resource metadata from 401 response");
+
+ // Create a more specific exception
+ var exception = new AuthorizationException("Authorization required but no resource metadata available")
+ {
+ ResourceUri = serverUri.ToString()
+ };
+ throw exception;
+ } // Store the resource metadata in the context before validating the resource URL
+ authContext.Value.ResourceMetadata = resourceMetadata;
+
+ // Validate that the resource matches the server FQDN
+ if (!authContext.Value.ValidateResourceUrl(serverUri))
+ {
+ _logger.LogWarning("Resource URL mismatch: expected {Expected}, got {Actual}",
+ serverUri, resourceMetadata.Resource);
+
+ var exception = new AuthorizationException($"Resource URL mismatch: expected {serverUri}, got {resourceMetadata.Resource}");
+ exception.ResourceUri = resourceMetadata.Resource.ToString();
+ throw exception;
+ }
+
+ // Get the first authorization server from the metadata
+ if (resourceMetadata.AuthorizationServers == null || resourceMetadata.AuthorizationServers.Length == 0)
+ { _logger.LogWarning("No authorization servers found in resource metadata");
+
+ var exception = new AuthorizationException("No authorization servers available");
+ exception.ResourceUri = resourceMetadata.Resource.ToString();
+ throw exception;
+ }
+
+ var authServerUrl = resourceMetadata.AuthorizationServers[0];
+ _logger.LogDebug("Using authorization server: {AuthServerUrl}", authServerUrl); try
+ {
+ // Discover authorization server metadata
+ var authServerMetadata = await AuthorizationService.DiscoverAuthorizationServerMetadataAsync(authServerUrl);
+ authContext.Value.AuthorizationServerMetadata = authServerMetadata;
+ _logger.LogDebug("Successfully retrieved authorization server metadata");
+
+ // Create client metadata
+ string[] redirectUris = _redirectUris?.ToArray() ?? new[] { "http://localhost:8888/callback" };
+ var clientMetadata = new ClientMetadata
+ {
+ RedirectUris = redirectUris,
+ ClientName = "MCP C# SDK Client",
+ Scope = string.Join(" ", _scopes ?? resourceMetadata.ScopesSupported ?? Array.Empty())
+ };
+
+ // Register client if needed, or use pre-configured client ID
+ if (!string.IsNullOrEmpty(_clientId))
+ {
+ _logger.LogDebug("Using pre-configured client ID: {ClientId}", _clientId);
+
+ // Create a client registration response to store in the context
+ var clientRegistration = new ClientRegistration
+ {
+ ClientId = _clientId!, // Using null-forgiving operator since we've already checked it's not null
+ ClientSecret = _clientSecret,
+ };
+
+ authContext.Value.ClientRegistration = clientRegistration;
+ }
+ else if (authServerMetadata.RegistrationEndpoint != null)
+ {
+ // Register client dynamically
+ _logger.LogDebug("Registering client with authorization server");
+ var clientRegistration = await AuthorizationService.RegisterClientAsync(authServerMetadata, clientMetadata);
+ authContext.Value.ClientRegistration = clientRegistration;
+ _logger.LogDebug("Client registered successfully with ID: {ClientId}", clientRegistration.ClientId);
+ }
+ else
+ {
+ _logger.LogWarning("Authorization server does not support dynamic client registration and no client ID was provided");
+ var exception = new AuthorizationException(
+ "Authorization server does not support dynamic client registration and no client ID was provided. " +
+ "Use McpAuthorizationOptions.ClientId to provide a pre-registered client ID.");
+ exception.ResourceUri = resourceMetadata.Resource.ToString();
+ exception.AuthorizationServerUri = authServerUrl.ToString();
+ throw exception;
+ }
+
+ // If we have no way to handle user authorization, we can't proceed
+ if (_authorizeCallback == null)
+ {
+ _logger.LogWarning("No authorization callback provided, can't proceed with OAuth flow");
+ var exception = new AuthorizationException(
+ "Authentication is required but no authorization callback was provided. " +
+ "Use McpAuthorizationOptions.AuthorizeCallback to provide a callback function.");
+ exception.ResourceUri = resourceMetadata.Resource.ToString();
+ exception.AuthorizationServerUri = authServerUrl.ToString();
+ throw exception;
+ }
+
+ // Generate PKCE values
+ var (codeVerifier, codeChallenge) = AuthorizationService.GeneratePkceValues();
+ authContext.Value.CodeVerifier = codeVerifier;
+
+ // Initiate authorization code flow
+ _logger.LogDebug("Initiating authorization code flow");
+
+ // Get the authorization URL that the user needs to visit
+ var authUrl = AuthorizationService.CreateAuthorizationUrl(
+ authServerMetadata,
+ authContext.Value.ClientRegistration.ClientId,
+ clientMetadata.RedirectUris[0],
+ codeChallenge,
+ _scopes?.ToArray() ?? resourceMetadata.ScopesSupported);
+
+ _logger.LogDebug("Authorization URL: {AuthUrl}", authUrl);
+
+ // Set the authorization URL in the client metadata
+ clientMetadata.ClientUri = authUrl;
+
+ // Let the callback handle the user authorization
+ var (redirectUri, code) = await _authorizeCallback(clientMetadata);
+ authContext.Value.RedirectUri = redirectUri;
+
+ // Exchange the code for tokens
+ _logger.LogDebug("Exchanging authorization code for tokens");
+ var tokenResponse = await AuthorizationService.ExchangeCodeForTokensAsync(
+ authServerMetadata,
+ authContext.Value.ClientRegistration.ClientId,
+ authContext.Value.ClientRegistration.ClientSecret,
+ redirectUri,
+ code,
+ codeVerifier);
+
+ authContext.Value.TokenResponse = tokenResponse;
+ authContext.Value.TokenIssuedAt = DateTimeOffset.UtcNow;
+
+ _logger.LogDebug("Successfully obtained access token");
+ return true;
+ }
+ catch (Exception ex) when (ex is not AuthorizationException)
+ {
+ _logger.LogError(ex, "Failed to complete authorization flow");
+ var authException = new AuthorizationException(
+ $"Failed to complete authorization flow: {ex.Message}", ex, McpErrorCode.InvalidRequest);
+
+ authException.ResourceUri = resourceMetadata.Resource.ToString();
+ authException.AuthorizationServerUri = authServerUrl.ToString();
+
+ throw authException;
+ }
+ }
+
+ private async Task GetValidTokenAsync()
+ {
+ using var authContext = await _authContext.LockAsync();
+
+ // If we have a valid token, use it
+ if (authContext.Value.HasValidToken)
+ {
+ _logger.LogDebug("Using existing valid access token");
+ return authContext.Value.GetAccessToken();
+ }
+
+ // If we can refresh the token, do so
+ if (authContext.Value.CanRefreshToken)
+ {
+ try
+ {
+ _logger.LogDebug("Refreshing access token");
+
+ // Null checks to ensure parameters are valid
+ if (authContext.Value.AuthorizationServerMetadata == null)
+ {
+ _logger.LogError("Cannot refresh token: AuthorizationServerMetadata is null");
+ return null;
+ }
+
+ if (authContext.Value.ClientRegistration == null)
+ {
+ _logger.LogError("Cannot refresh token: ClientRegistration is null");
+ return null;
+ }
+
+ if (authContext.Value.TokenResponse?.RefreshToken == null)
+ {
+ _logger.LogError("Cannot refresh token: RefreshToken is null");
+ return null;
+ }
+
+ var tokenResponse = await AuthorizationService.RefreshTokenAsync(
+ authContext.Value.AuthorizationServerMetadata,
+ authContext.Value.ClientRegistration.ClientId,
+ authContext.Value.ClientRegistration.ClientSecret,
+ authContext.Value.TokenResponse.RefreshToken);
+
+ authContext.Value.TokenResponse = tokenResponse;
+ authContext.Value.TokenIssuedAt = DateTimeOffset.UtcNow;
+
+ _logger.LogDebug("Successfully refreshed access token");
+ return tokenResponse.AccessToken;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to refresh access token");
+ // Clear the token so we'll try to reauthenticate on the next request
+ authContext.Value.TokenResponse = null;
+ authContext.Value.TokenIssuedAt = null;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/IAuthorizationHandler.cs b/src/ModelContextProtocol/Protocol/Auth/IAuthorizationHandler.cs
new file mode 100644
index 00000000..ffa41acb
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/IAuthorizationHandler.cs
@@ -0,0 +1,22 @@
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Defines methods for handling authorization in an MCP client.
+///
+public interface IAuthorizationHandler
+{
+ ///
+ /// Handles authentication for HTTP requests.
+ ///
+ /// The HTTP request to authenticate.
+ /// A representing the asynchronous operation.
+ Task AuthenticateRequestAsync(HttpRequestMessage request);
+
+ ///
+ /// Handles a 401 Unauthorized response.
+ ///
+ /// The HTTP response that contains the 401 status code.
+ /// The URI of the server that returned the 401 status code.
+ /// A that represents the asynchronous operation. The task result contains true if the authentication was successful and the request should be retried, otherwise false.
+ Task HandleUnauthorizedResponseAsync(HttpResponseMessage response, Uri serverUri);
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/IServerAuthorizationProvider.cs b/src/ModelContextProtocol/Protocol/Auth/IServerAuthorizationProvider.cs
new file mode 100644
index 00000000..4aee3f22
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/IServerAuthorizationProvider.cs
@@ -0,0 +1,24 @@
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Defines the interface for MCP server authorization providers.
+///
+///
+/// This interface is implemented by authorization providers that enable MCP servers to validate tokens
+/// and control access to protected resources.
+///
+public interface IServerAuthorizationProvider
+{
+ ///
+ /// Gets the Protected Resource Metadata (PRM) for the server.
+ ///
+ /// The protected resource metadata.
+ ProtectedResourceMetadata GetProtectedResourceMetadata();
+
+ ///
+ /// Validates the provided authorization token.
+ ///
+ /// The authorization header value.
+ /// A representing the asynchronous validation operation. The task result contains if the token is valid; otherwise, .
+ Task ValidateTokenAsync(string authorizationHeader);
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/ProtectedResourceMetadata.cs b/src/ModelContextProtocol/Protocol/Auth/ProtectedResourceMetadata.cs
new file mode 100644
index 00000000..e80d58ad
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/ProtectedResourceMetadata.cs
@@ -0,0 +1,45 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Represents the Protected Resource Metadata (PRM) document for an OAuth 2.0 protected resource.
+///
+///
+/// The PRM document describes the properties and requirements of a protected resource, including
+/// the authorization servers that can be used to obtain access tokens and the scopes that are supported.
+/// This document is served at the standard path "/.well-known/oauth-protected-resource" by MCP servers
+/// that have authorization enabled.
+///
+public class ProtectedResourceMetadata
+{
+ ///
+ /// Gets or sets the resource identifier URI.
+ ///
+ [JsonPropertyName("resource")]
+ public required Uri Resource { get; set; }
+
+ ///
+ /// Gets or sets the authorization servers that can be used for authentication.
+ ///
+ [JsonPropertyName("authorization_servers")]
+ public required Uri[] AuthorizationServers { get; set; }
+
+ ///
+ /// Gets or sets the bearer token methods supported by the resource.
+ ///
+ [JsonPropertyName("bearer_methods_supported")]
+ public string[]? BearerMethodsSupported { get; set; } = ["header"];
+
+ ///
+ /// Gets or sets the scopes supported by the resource.
+ ///
+ [JsonPropertyName("scopes_supported")]
+ public string[]? ScopesSupported { get; set; }
+
+ ///
+ /// Gets or sets the URL to the resource documentation.
+ ///
+ [JsonPropertyName("resource_documentation")]
+ public Uri? ResourceDocumentation { get; set; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Auth/Token.cs b/src/ModelContextProtocol/Protocol/Auth/Token.cs
new file mode 100644
index 00000000..f9559068
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Auth/Token.cs
@@ -0,0 +1,39 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Auth;
+
+///
+/// Represents the OAuth 2.0 token response as defined in RFC 6749.
+///
+public class Token
+{
+ ///
+ /// Gets or sets the access token issued by the authorization server.
+ ///
+ [JsonPropertyName("access_token")]
+ public required string AccessToken { get; set; }
+
+ ///
+ /// Gets or sets the type of the token issued.
+ ///
+ [JsonPropertyName("token_type")]
+ public required string TokenType { get; set; }
+
+ ///
+ /// Gets or sets the lifetime in seconds of the access token.
+ ///
+ [JsonPropertyName("expires_in")]
+ public long? ExpiresIn { get; set; }
+
+ ///
+ /// Gets or sets the refresh token, which can be used to obtain new access tokens.
+ ///
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; set; }
+
+ ///
+ /// Gets or sets the scope of the access token.
+ ///
+ [JsonPropertyName("scope")]
+ public string? Scope { get; set; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs b/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs
index 1b286557..fc8a35e5 100644
--- a/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs
+++ b/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Protocol.Auth;
using ModelContextProtocol.Utils;
namespace ModelContextProtocol.Protocol.Transport;
@@ -7,10 +8,15 @@ namespace ModelContextProtocol.Protocol.Transport;
/// Provides an over HTTP using the Server-Sent Events (SSE) protocol.
///
///
+///
/// This transport connects to an MCP server over HTTP using SSE,
/// allowing for real-time server-to-client communication with a standard HTTP request.
/// Unlike the , this transport connects to an existing server
/// rather than launching a new process.
+///
+///
+/// The SSE transport can handle OAuth 2.0 authorization flows when connecting to servers that require authentication.
+///
///
public sealed class SseClientTransport : IClientTransport, IAsyncDisposable
{
diff --git a/src/ModelContextProtocol/Protocol/Transport/SseClientTransportOptions.cs b/src/ModelContextProtocol/Protocol/Transport/SseClientTransportOptions.cs
index b83204ae..c696c664 100644
--- a/src/ModelContextProtocol/Protocol/Transport/SseClientTransportOptions.cs
+++ b/src/ModelContextProtocol/Protocol/Transport/SseClientTransportOptions.cs
@@ -1,5 +1,7 @@
namespace ModelContextProtocol.Protocol.Transport;
+using ModelContextProtocol.Protocol.Auth;
+
///
/// Provides options for configuring instances.
///
@@ -62,4 +64,35 @@ public required Uri Endpoint
/// Use this property to specify custom HTTP headers that should be sent with each request to the server.
///
public Dictionary? AdditionalHeaders { get; init; }
+
+ ///
+ /// Gets or sets the authorization options to use when connecting to the SSE server.
+ ///
+ ///
+ ///
+ /// These options configure the behavior of client-side authorization with the SSE server.
+ ///
+ ///
+ /// You can use this to specify a callback for handling the authorization code flow,
+ /// provide pre-registered client credentials, or configure other aspects of the OAuth flow.
+ ///
+ ///
+ /// Example:
+ ///
+ /// var transportOptions = new SseClientTransportOptions
+ /// { /// Endpoint = new Uri("http://localhost:7071/sse"),
+ /// AuthorizationOptions = new McpAuthorizationOptions
+ /// {
+ /// ClientId = "my-client-id",
+ /// ClientSecret = "my-client-secret",
+ /// RedirectUris = new[] { "http://localhost:8888/callback" },
+ /// AuthorizeCallback = AuthorizationService.CreateHttpListenerAuthorizeCallback(
+ /// openBrowser: url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true })
+ /// )
+ /// }
+ /// };
+ ///
+ ///
+ ///
+ public AuthorizationOptions? AuthorizationOptions { get; init; }
}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/AuthorizationCapability.cs b/src/ModelContextProtocol/Protocol/Types/AuthorizationCapability.cs
new file mode 100644
index 00000000..47557b00
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/AuthorizationCapability.cs
@@ -0,0 +1,19 @@
+using ModelContextProtocol.Protocol.Auth;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// Defines the capabilities of a server for supporting OAuth 2.0 authorization.
+///
+///
+/// This capability is advertised by servers that support OAuth 2.0 authorization flows
+/// and require clients to authenticate using bearer tokens.
+///
+public class AuthorizationCapability
+{
+ ///
+ /// Gets or sets the authorization provider that handles token validation and provides
+ /// metadata about the protected resource.
+ ///
+ public IServerAuthorizationProvider? AuthorizationProvider { get; set; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Server/Auth/BasicServerAuthorizationProvider.cs b/src/ModelContextProtocol/Server/Auth/BasicServerAuthorizationProvider.cs
new file mode 100644
index 00000000..48950ea1
--- /dev/null
+++ b/src/ModelContextProtocol/Server/Auth/BasicServerAuthorizationProvider.cs
@@ -0,0 +1,47 @@
+using ModelContextProtocol.Protocol.Auth;
+
+namespace ModelContextProtocol.Server.Auth;
+
+///
+/// A basic implementation of .
+///
+///
+/// This implementation is intended as a starting point for server developers. In production environments,
+/// it should be extended or replaced with a more robust implementation that integrates with your
+/// authentication system (e.g., OAuth 2.0 server, identity provider, etc.)
+///
+///
+/// Initializes a new instance of the class
+/// with the specified resource metadata and token validator.
+///
+/// The protected resource metadata.
+/// A function that validates access tokens. If not provided, a function that always returns true will be used.
+public class BasicServerAuthorizationProvider(
+ ProtectedResourceMetadata resourceMetadata,
+ Func>? tokenValidator = null) : IServerAuthorizationProvider
+{
+ private readonly ProtectedResourceMetadata _resourceMetadata = resourceMetadata ?? throw new ArgumentNullException(nameof(resourceMetadata));
+ private readonly Func> _tokenValidator = tokenValidator ?? (_ => Task.FromResult(true));
+
+ ///
+ public ProtectedResourceMetadata GetProtectedResourceMetadata() => _resourceMetadata;
+
+ ///
+ public async Task ValidateTokenAsync(string authorizationHeader)
+ {
+ // Extract the token from the Authorization header
+ if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var token = authorizationHeader["Bearer ".Length..].Trim();
+ if (string.IsNullOrEmpty(token))
+ {
+ return false;
+ }
+
+ // Validate the token
+ return await _tokenValidator(token);
+ }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Server/McpServer.cs b/src/ModelContextProtocol/Server/McpServer.cs
index ae0e7afc..7faad5c9 100644
--- a/src/ModelContextProtocol/Server/McpServer.cs
+++ b/src/ModelContextProtocol/Server/McpServer.cs
@@ -66,6 +66,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
SetResourcesHandler(options);
SetSetLoggingLevelHandler(options);
SetCompletionHandler(options);
+ SetAuthorizationHandler();
SetPingHandler();
// Register any notification handlers that were provided.
@@ -503,6 +504,13 @@ private void SetSetLoggingLevelHandler(McpServerOptions options)
McpJsonUtilities.JsonContext.Default.EmptyResult);
}
+ private void SetAuthorizationHandler()
+ {
+ // The authorization capability is handled via middleware in ASP.NET Core,
+ // so we don't need to set up any special handlers here.
+ // We just make sure to include the capability in the ServerCapabilities.
+ }
+
private ValueTask InvokeHandlerAsync(
Func, CancellationToken, ValueTask> handler,
TParams? args,
diff --git a/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs b/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs
index b759ba97..7b33940b 100644
--- a/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs
+++ b/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs
@@ -122,6 +122,13 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(SubscribeRequestParams))]
[JsonSerializable(typeof(UnsubscribeRequestParams))]
[JsonSerializable(typeof(IReadOnlyDictionary))]
+
+ // Authorization-related types
+ [JsonSerializable(typeof(Protocol.Auth.ProtectedResourceMetadata))]
+ [JsonSerializable(typeof(Protocol.Auth.AuthorizationServerMetadata))]
+ [JsonSerializable(typeof(Protocol.Auth.ClientMetadata))]
+ [JsonSerializable(typeof(Protocol.Auth.ClientRegistration))]
+ [JsonSerializable(typeof(Protocol.Auth.Token))]
[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
diff --git a/src/ModelContextProtocol/Utils/SynchronizedValue.cs b/src/ModelContextProtocol/Utils/SynchronizedValue.cs
new file mode 100644
index 00000000..106bf29b
--- /dev/null
+++ b/src/ModelContextProtocol/Utils/SynchronizedValue.cs
@@ -0,0 +1,75 @@
+namespace ModelContextProtocol.Utils;
+
+///
+/// Provides a thread-safe synchronized value with locking functionality.
+///
+/// The type of value to synchronize.
+internal class SynchronizedValue where T : class
+{
+ private readonly SemaphoreSlim _semaphore = new(1, 1);
+ private T _value;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The initial value.
+ public SynchronizedValue(T initialValue)
+ {
+ _value = initialValue;
+ }
+
+ ///
+ /// Gets the current value without locking.
+ ///
+ ///
+ /// This property should only be used when thread safety is not required.
+ ///
+ public T UnsafeValue => _value;
+
+ ///
+ /// Acquires a lock on the value and provides access to it.
+ ///
+ /// A disposable that provides access to the value and releases the lock when disposed.
+ public async Task LockAsync()
+ {
+ await _semaphore.WaitAsync().ConfigureAwait(false);
+ return new SynchronizedValueHandle(this);
+ }
+
+ ///
+ /// Provides a handle to access the synchronized value while holding a lock.
+ ///
+ public class SynchronizedValueHandle : IDisposable
+ {
+ private readonly SynchronizedValue _parent;
+ private bool _disposed;
+
+ internal SynchronizedValueHandle(SynchronizedValue parent)
+ {
+ _parent = parent;
+ }
+
+ ///
+ /// Gets or sets the synchronized value.
+ ///
+ public T Value
+ {
+ get => _parent._value;
+ set => _parent._value = value;
+ }
+
+ ///
+ /// Releases the lock on the synchronized value.
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _parent._semaphore.Release();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ModelContextProtocol.Tests/Protocol/Auth/ProtectedResourceMetadataTests.cs b/tests/ModelContextProtocol.Tests/Protocol/Auth/ProtectedResourceMetadataTests.cs
new file mode 100644
index 00000000..e4af9c91
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Protocol/Auth/ProtectedResourceMetadataTests.cs
@@ -0,0 +1,61 @@
+using ModelContextProtocol.Protocol.Auth;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Protocol.Auth;
+
+public class ProtectedResourceMetadataTests
+{
+ [Fact]
+ public void ProtectedResourceMetadata_JsonSerialization_Works()
+ {
+ // Arrange
+ var metadata = new ProtectedResourceMetadata
+ {
+ Resource = new Uri("http://localhost:7071"),
+ AuthorizationServers = [new Uri("https://login.microsoftonline.com/tenant/v2.0")],
+ BearerMethodsSupported = ["header"],
+ ScopesSupported = ["mcp.tools", "mcp.prompts"],
+ ResourceDocumentation = new Uri("https://example.com/docs")
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(metadata);
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(metadata.Resource, deserialized.Resource);
+ Assert.Equal(metadata.AuthorizationServers[0], deserialized.AuthorizationServers[0]);
+ Assert.Equal("header", deserialized.BearerMethodsSupported![0]);
+ Assert.Equal(2, deserialized.ScopesSupported!.Length);
+ Assert.Contains("mcp.tools", deserialized.ScopesSupported!);
+ Assert.Contains("mcp.prompts", deserialized.ScopesSupported!);
+ Assert.Equal(metadata.ResourceDocumentation, deserialized.ResourceDocumentation);
+ }
+
+ [Fact]
+ public void ProtectedResourceMetadata_JsonDeserialization_WorksWithStringProperties()
+ {
+ // Arrange
+ var json = @"{
+ ""resource"": ""http://localhost:7071"",
+ ""authorization_servers"": [""https://login.microsoftonline.com/tenant/v2.0""],
+ ""bearer_methods_supported"": [""header""],
+ ""scopes_supported"": [""mcp.tools"", ""mcp.prompts""],
+ ""resource_documentation"": ""https://example.com/docs""
+ }";
+
+ // Act
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(new Uri("http://localhost:7071"), deserialized.Resource);
+ Assert.Equal(new Uri("https://login.microsoftonline.com/tenant/v2.0"), deserialized.AuthorizationServers[0]);
+ Assert.Equal("header", deserialized.BearerMethodsSupported![0]);
+ Assert.Equal(2, deserialized.ScopesSupported!.Length);
+ Assert.Contains("mcp.tools", deserialized.ScopesSupported!);
+ Assert.Contains("mcp.prompts", deserialized.ScopesSupported!);
+ Assert.Equal(new Uri("https://example.com/docs"), deserialized.ResourceDocumentation);
+ }
+}