diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 515472817..fdf8cece3 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T | Diagnostic ID | Description | | :------------ | :---------- | -| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). | +| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks, Extensions, and the `application_type` parameter in Dynamic Client Registration. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). The `application_type` parameter in Dynamic Client Registration is part of a future MCP specification version (see [SEP-837](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837)). | | `MCPEXP002` | Experimental SDK APIs unrelated to the MCP specification itself, including subclassing `McpClient`/`McpServer` (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)) and `RunSessionHandler`, which may be removed or change signatures in a future release (consider using `ConfigureSessionOptions` instead). | ## Obsolete APIs diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs index 7e7e969bb..dec2e05af 100644 --- a/src/Common/Experimentals.cs +++ b/src/Common/Experimentals.cs @@ -110,4 +110,23 @@ internal static class Experimentals /// URL for the experimental RunSessionHandler API. /// public const string RunSessionHandler_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp002"; + + /// + /// Diagnostic ID for the experimental application_type parameter in Dynamic Client Registration per SEP-837. + /// + /// + /// This uses the same diagnostic ID as because it is an experimental + /// feature in the MCP specification itself. + /// + public const string DcrApplicationType_DiagnosticId = "MCPEXP001"; + + /// + /// Message for the experimental application_type parameter in Dynamic Client Registration. + /// + public const string DcrApplicationType_Message = "The application_type parameter in Dynamic Client Registration is part of a future MCP specification version (SEP-837) and is subject to change."; + + /// + /// URL for the experimental application_type parameter in Dynamic Client Registration. + /// + public const string DcrApplicationType_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001"; } diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index ecef8e15e..331778bf3 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -33,11 +33,12 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; private readonly Uri? _clientMetadataDocumentUri; - // _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591) + // _dcrClientName, _dcrClientUri, _dcrInitialAccessToken, _dcrResponseDelegate and _dcrApplicationType are used for dynamic client registration (RFC 7591) private readonly string? _dcrClientName; private readonly Uri? _dcrClientUri; private readonly string? _dcrInitialAccessToken; private readonly Func? _dcrResponseDelegate; + private readonly string? _dcrApplicationType; private readonly HttpClient _httpClient; private readonly ILogger _logger; @@ -89,6 +90,9 @@ public ClientOAuthProvider( _dcrClientUri = options.DynamicClientRegistration?.ClientUri; _dcrInitialAccessToken = options.DynamicClientRegistration?.InitialAccessToken; _dcrResponseDelegate = options.DynamicClientRegistration?.ResponseDelegate; +#pragma warning disable MCPEXP001 // application_type in DCR is experimental per SEP-837 + _dcrApplicationType = options.DynamicClientRegistration?.ApplicationType; +#pragma warning restore MCPEXP001 _tokenCache = options.TokenCache ?? new InMemoryTokenCache(); } @@ -654,6 +658,7 @@ private async Task PerformDynamicClientRegistrationAsync( ClientName = _dcrClientName, ClientUri = _dcrClientUri?.ToString(), Scope = GetScopeParameter(protectedResourceMetadata), + ApplicationType = _dcrApplicationType ?? (IsLocalhostRedirectUri(_redirectUri) ? "native" : "web"), }; var requestBytes = JsonSerializer.SerializeToUtf8Bytes(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest); @@ -712,6 +717,10 @@ private async Task PerformDynamicClientRegistrationAsync( private static string? GetResourceUri(ProtectedResourceMetadata protectedResourceMetadata) => protectedResourceMetadata.Resource; + private static bool IsLocalhostRedirectUri(Uri redirectUri) + => redirectUri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) + || (System.Net.IPAddress.TryParse(redirectUri.Host, out var ipAddress) && System.Net.IPAddress.IsLoopback(ipAddress)); + private string? GetScopeParameter(ProtectedResourceMetadata protectedResourceMetadata) { if (!string.IsNullOrEmpty(protectedResourceMetadata.WwwAuthenticateScope)) diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs index 5d145a568..de378994e 100644 --- a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace ModelContextProtocol.Authentication; /// @@ -46,4 +48,21 @@ public sealed class DynamicClientRegistrationOptions /// /// public Func? ResponseDelegate { get; set; } + + /// + /// Gets or sets the application type to use during dynamic client registration. + /// + /// + /// + /// Valid values are "native" and "web". If not specified, the application type will be + /// automatically determined based on the redirect URI: "native" for localhost/127.0.0.1 + /// redirect URIs, "web" for all others. + /// + /// + /// Per the MCP specification, native applications (desktop, mobile, CLI, localhost web apps) + /// should use "native", and web applications (remote browser-based) should use "web". + /// + /// + [Experimental(Experimentals.DcrApplicationType_DiagnosticId, UrlFormat = Experimentals.DcrApplicationType_Url)] + public string? ApplicationType { get; set; } } diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationRequest.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationRequest.cs index 8496610e7..69c0797d3 100644 --- a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationRequest.cs +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationRequest.cs @@ -48,4 +48,17 @@ internal sealed class DynamicClientRegistrationRequest /// [JsonPropertyName("scope")] public string? Scope { get; init; } + + /// + /// Gets or sets the application type for the client, as defined in OpenID Connect Dynamic Client Registration 1.0. + /// + /// + /// Valid values are "native" and "web". Per the MCP specification, MCP clients MUST specify an appropriate + /// application type during Dynamic Client Registration. This field is automatically populated by the SDK + /// based on the redirect URI if not explicitly set via . + /// Native applications (desktop, mobile, CLI, localhost web apps) should use "native". + /// Web applications (remote browser-based) should use "web". + /// + [JsonPropertyName("application_type")] + public string? ApplicationType { get; init; } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs index c4979fb10..24b14c296 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs @@ -1261,4 +1261,84 @@ public async Task CanAuthenticate_WithLegacyServerUsingDefaultEndpointFallback() await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); } + + [Fact] + public async Task DynamicClientRegistration_SendsNativeApplicationType_ForLocalhostRedirectUri() + { + await using var app = await StartMcpServerAsync(); + + await using var transport = new HttpClientTransport(new() + { + Endpoint = new(McpServerUrl), + OAuth = new ClientOAuthOptions() + { + RedirectUri = new Uri("http://localhost:1179/callback"), + AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + DynamicClientRegistration = new() + { + ClientName = "Test MCP Client", + }, + }, + }, HttpClient, LoggerFactory); + + await using var client = await McpClient.CreateAsync( + transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("native", TestOAuthServer.LastRegistrationApplicationType); + } + + [Fact] + public async Task DynamicClientRegistration_SendsWebApplicationType_ForNonLocalhostRedirectUri() + { + await using var app = await StartMcpServerAsync(); + + await using var transport = new HttpClientTransport(new() + { + Endpoint = new(McpServerUrl), + OAuth = new ClientOAuthOptions() + { + RedirectUri = new Uri("https://myapp.example.com/callback"), + AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + DynamicClientRegistration = new() + { + ClientName = "Test MCP Client", + }, + }, + }, HttpClient, LoggerFactory); + + await using var client = await McpClient.CreateAsync( + transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("web", TestOAuthServer.LastRegistrationApplicationType); + } + + [Fact] +#pragma warning disable MCPEXP001 // application_type in DCR is experimental per SEP-837 + public async Task DynamicClientRegistration_UsesExplicitApplicationType_WhenConfigured() + { + await using var app = await StartMcpServerAsync(); + + await using var transport = new HttpClientTransport(new() + { + Endpoint = new(McpServerUrl), + OAuth = new ClientOAuthOptions() + { + // localhost redirect URI would normally auto-detect as "native", + // but the explicit setting should override it. + RedirectUri = new Uri("http://localhost:1179/callback"), + AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + DynamicClientRegistration = new() + { + ClientName = "Test MCP Client", + ApplicationType = "web", + }, + }, + }, HttpClient, LoggerFactory); +#pragma warning restore MCPEXP001 + + await using var client = await McpClient.CreateAsync( + transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("web", TestOAuthServer.LastRegistrationApplicationType); + } } diff --git a/tests/ModelContextProtocol.TestOAuthServer/ClientRegistrationRequest.cs b/tests/ModelContextProtocol.TestOAuthServer/ClientRegistrationRequest.cs index 50592bbea..62567da94 100644 --- a/tests/ModelContextProtocol.TestOAuthServer/ClientRegistrationRequest.cs +++ b/tests/ModelContextProtocol.TestOAuthServer/ClientRegistrationRequest.cs @@ -55,6 +55,12 @@ internal sealed class ClientRegistrationRequest [JsonPropertyName("scope")] public string? Scope { get; init; } + /// + /// Gets or sets the application type. + /// + [JsonPropertyName("application_type")] + public string? ApplicationType { get; init; } + /// /// Gets or sets the contacts for the client. /// diff --git a/tests/ModelContextProtocol.TestOAuthServer/Program.cs b/tests/ModelContextProtocol.TestOAuthServer/Program.cs index e882ecbef..38748e2f1 100644 --- a/tests/ModelContextProtocol.TestOAuthServer/Program.cs +++ b/tests/ModelContextProtocol.TestOAuthServer/Program.cs @@ -81,6 +81,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor public HashSet DisabledMetadataPaths { get; } = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyCollection MetadataRequests => _metadataRequests.ToArray(); + /// + /// Gets the application type from the most recent dynamic client registration request received by the server. + /// + public string? LastRegistrationApplicationType { get; private set; } + /// /// Entry point for the application. /// @@ -501,6 +506,8 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null) }); } + LastRegistrationApplicationType = registrationRequest.ApplicationType; + // Validate redirect URIs are provided if (registrationRequest.RedirectUris.Count == 0) {