Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the
| `MCP9004` | In place | <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. |
| `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information. |
| `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions> — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. Starting with the `2026-07-28` protocol revision, Streamable HTTP no longer supports sessions, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. |
| `MCP9007` | In place | `AuthorizationRedirectDelegate` and `ClientOAuthOptions.AuthorizationRedirectDelegate` are retained for source and binary compatibility but cannot provide the RFC 9207 authorization-response issuer. Use `ClientOAuthOptions.AuthorizationCallbackHandler` for issuer-aware authorization flows. |
18 changes: 12 additions & 6 deletions samples/ProtectedMcpClient/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Authentication;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using System.Diagnostics;
Expand Down Expand Up @@ -32,7 +33,7 @@
OAuth = new()
{
RedirectUri = new Uri("http://localhost:1179/callback"),
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
DynamicClientRegistration = new()
{
ClientName = "ProtectedMcpClient",
Expand Down Expand Up @@ -68,12 +69,16 @@
/// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
/// This implementation demonstrates how SDK consumers can provide their own authorization flow.
/// </summary>
/// <param name="authorizationUrl">The authorization URL to open in the browser.</param>
/// <param name="redirectUri">The redirect URI where the authorization code will be sent.</param>
/// <param name="authorizationContext">The context containing the authorization and redirect URIs.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The authorization code extracted from the callback, or null if the operation failed.</returns>
static async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
/// <returns>The authorization result extracted from the callback, or null if the operation failed.</returns>
static async Task<AuthorizationResult?> HandleAuthorizationUrlAsync(
AuthorizationCallbackContext authorizationContext,
CancellationToken cancellationToken)
{
var authorizationUrl = authorizationContext.AuthorizationUri;
var redirectUri = authorizationContext.RedirectUri;

Console.WriteLine("Starting OAuth authorization flow...");
Console.WriteLine($"Opening browser to: {authorizationUrl}");

Expand All @@ -93,6 +98,7 @@
var context = await listener.GetContextAsync();
var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
var code = query["code"];
var iss = query["iss"];
var error = query["error"];

string responseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>";
Expand All @@ -115,7 +121,7 @@
}

Console.WriteLine("Authorization code received successfully.");
return code;
return new AuthorizationResult { Code = code, Iss = iss };
}
catch (Exception ex)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Common/Obsoletions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ internal static class Obsoletions
public const string LegacyStatefulHttp_DiagnosticId = "MCP9006";
public const string LegacyStatefulHttp_Message = "Stateful Streamable HTTP mode is a back-compat-only escape hatch for legacy clients. Set HttpServerTransportOptions.Stateless = true (the default as of the 2026-07-28 protocol revision) for new code. See SEP-2567.";
public const string LegacyStatefulHttp_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#obsolete-apis";

public const string AuthorizationRedirectDelegate_DiagnosticId = "MCP9007";
public const string AuthorizationRedirectDelegate_Message = "AuthorizationRedirectDelegate cannot provide the RFC 9207 issuer and is retained for compatibility only. Use AuthorizationCallbackHandler instead.";
public const string AuthorizationRedirectDelegate_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#obsolete-apis";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ModelContextProtocol.Authentication;

/// <summary>
/// Provides the information needed to complete an OAuth authorization request.
/// </summary>
public sealed class AuthorizationCallbackContext
{
/// <summary>
/// Gets the authorization URI that the user needs to visit.
/// </summary>
public required Uri AuthorizationUri { get; init; }

/// <summary>
/// Gets the redirect URI where the authorization response will be sent.
/// </summary>
public required Uri RedirectUri { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

namespace ModelContextProtocol.Authentication;

/// <summary>
Expand All @@ -9,20 +8,13 @@ namespace ModelContextProtocol.Authentication;
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <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>
/// <remarks>
/// <para>
/// This delegate provides SDK consumers with full control over how the OAuth authorization flow is handled.
/// Implementers can choose to:
/// </para>
/// <list type="bullet">
/// <item><description>Start a local HTTP server and open a browser (default behavior)</description></item>
/// <item><description>Display the authorization URL to the user for manual handling</description></item>
/// <item><description>Integrate with a custom UI or authentication flow</description></item>
/// <item><description>Use a different redirect mechanism altogether</description></item>
/// </list>
/// <para>
/// The implementation should handle user interaction to visit the authorization URL and extract
/// the authorization code from the callback. The authorization code is typically provided as
/// a query parameter in the redirect URI callback.
/// </para>
/// This delegate cannot return the <c>iss</c> parameter from the authorization response, so
/// <see href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</see> issuer validation is
/// skipped when it is used. Use <see cref="ClientOAuthOptions.AuthorizationCallbackHandler"/> for
/// issuer-aware authorization flows.
/// </remarks>
public delegate Task<string?> AuthorizationRedirectDelegate(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken);
[Obsolete(
ModelContextProtocol.Obsoletions.AuthorizationRedirectDelegate_Message,
DiagnosticId = ModelContextProtocol.Obsoletions.AuthorizationRedirectDelegate_DiagnosticId,
UrlFormat = ModelContextProtocol.Obsoletions.AuthorizationRedirectDelegate_Url)]
public delegate Task<string?> AuthorizationRedirectDelegate(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace ModelContextProtocol.Authentication;

/// <summary>
/// Represents the result of an OAuth authorization redirect, containing the authorization code
/// and optionally the issuer identifier from the authorization response.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="Iss"/> property should be populated from the <c>iss</c> query parameter in the
/// redirect URI when present, as specified by
/// <see href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</see>.
/// This enables the SDK to validate that the authorization response originated from the expected
/// authorization server, mitigating mix-up attacks.
/// </para>
/// </remarks>
public sealed class AuthorizationResult
{
/// <summary>
/// Gets the authorization code returned by the authorization server.
/// </summary>
public string? Code { get; init; }

/// <summary>
/// Gets the issuer identifier returned in the authorization response per
/// <see href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</see>.
/// </summary>
/// <remarks>
/// <para>
/// This value should be extracted from the <c>iss</c> query parameter of the redirect URI.
/// When present, the SDK validates it against the expected authorization server issuer to
/// prevent mix-up attacks.
/// </para>
/// <para>
/// Implementations of <see cref="ClientOAuthOptions.AuthorizationCallbackHandler"/> should populate this
/// property whenever the <c>iss</c> parameter is present in the redirect URI callback.
/// </para>
/// </remarks>
public string? Iss { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,11 @@ internal sealed class AuthorizationServerMetadata
/// </summary>
[JsonPropertyName("client_id_metadata_document_supported")]
public bool ClientIdMetadataDocumentSupported { get; set; }

/// <summary>
/// Indicates whether the authorization server includes the <c>iss</c> parameter in authorization responses
/// as defined in <see href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</see>.
/// </summary>
[JsonPropertyName("authorization_response_iss_parameter_supported")]
public bool AuthorizationResponseIssParameterSupported { get; set; }
}
34 changes: 30 additions & 4 deletions src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,44 @@ public sealed class ClientOAuthOptions
public ScopeSelectorDelegate? ScopeSelector { get; set; }

/// <summary>
/// Gets or sets the authorization redirect delegate for handling the OAuth authorization flow.
/// Gets or sets the callback that handles the OAuth authorization flow.
/// </summary>
/// <remarks>
/// <para>
/// This delegate is responsible for handling the OAuth authorization URL and obtaining the authorization code.
/// If not specified, a default implementation will be used that prompts the user to enter the code manually.
/// This callback receives the authorization and redirect URIs in an
/// <see cref="AuthorizationCallbackContext"/> and returns the authorization response.
/// If not specified, a default implementation prompts the user to enter the full redirect URL manually.
/// </para>
/// <para>
/// Custom implementations might open a browser, start an HTTP listener, or use other mechanisms to capture
/// the authorization code from the OAuth redirect.
/// the authorization response. They should return both the <c>code</c> and <c>iss</c> query parameters
/// from the redirect URI callback. This enables the SDK to validate the <c>iss</c> parameter per
/// <see href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</see>, which mitigates
/// mix-up attacks.
/// </para>
/// <para>
/// This property cannot be configured together with <see cref="AuthorizationRedirectDelegate"/>.
/// </para>
/// </remarks>
public Func<AuthorizationCallbackContext, CancellationToken, Task<AuthorizationResult?>>? AuthorizationCallbackHandler { get; set; }

/// <summary>
/// Gets or sets the legacy authorization redirect delegate for handling the OAuth authorization flow.
/// </summary>
/// <remarks>
/// <para>
/// This delegate returns only the authorization code and cannot provide the <c>iss</c> parameter from
/// the authorization response. Consequently, RFC 9207 issuer validation is skipped when this delegate
/// is used. Use <see cref="AuthorizationCallbackHandler"/> for issuer-aware authorization flows.
/// </para>
/// <para>
/// This property cannot be configured together with <see cref="AuthorizationCallbackHandler"/>.
/// </para>
/// </remarks>
[Obsolete(
ModelContextProtocol.Obsoletions.AuthorizationRedirectDelegate_Message,
DiagnosticId = ModelContextProtocol.Obsoletions.AuthorizationRedirectDelegate_DiagnosticId,
UrlFormat = ModelContextProtocol.Obsoletions.AuthorizationRedirectDelegate_Url)]
public AuthorizationRedirectDelegate? AuthorizationRedirectDelegate { get; set; }

/// <summary>
Expand Down
Loading
Loading