Skip to content

Commit 20e4dec

Browse files
Copilotstephentoub
andauthored
Add application_type support to dynamic client registration per MCP spec SEP-837
Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/8a200407-8cc6-4c69-97e0-c9a6ad509b93 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 044e279 commit 20e4dec

File tree

6 files changed

+128
-2
lines changed

6 files changed

+128
-2
lines changed

src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
3333
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
3434
private readonly Uri? _clientMetadataDocumentUri;
3535

36-
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
36+
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken, _dcrResponseDelegate and _dcrApplicationType are used for dynamic client registration (RFC 7591)
3737
private readonly string? _dcrClientName;
3838
private readonly Uri? _dcrClientUri;
3939
private readonly string? _dcrInitialAccessToken;
4040
private readonly Func<DynamicClientRegistrationResponse, CancellationToken, Task>? _dcrResponseDelegate;
41+
private readonly string? _dcrApplicationType;
4142

4243
private readonly HttpClient _httpClient;
4344
private readonly ILogger _logger;
@@ -89,6 +90,7 @@ public ClientOAuthProvider(
8990
_dcrClientUri = options.DynamicClientRegistration?.ClientUri;
9091
_dcrInitialAccessToken = options.DynamicClientRegistration?.InitialAccessToken;
9192
_dcrResponseDelegate = options.DynamicClientRegistration?.ResponseDelegate;
93+
_dcrApplicationType = options.DynamicClientRegistration?.ApplicationType;
9294
_tokenCache = options.TokenCache ?? new InMemoryTokenCache();
9395
}
9496

@@ -654,6 +656,7 @@ private async Task PerformDynamicClientRegistrationAsync(
654656
ClientName = _dcrClientName,
655657
ClientUri = _dcrClientUri?.ToString(),
656658
Scope = GetScopeParameter(protectedResourceMetadata),
659+
ApplicationType = _dcrApplicationType ?? (IsLocalhostRedirectUri(_redirectUri) ? "native" : "web"),
657660
};
658661

659662
var requestBytes = JsonSerializer.SerializeToUtf8Bytes(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest);
@@ -712,6 +715,11 @@ private async Task PerformDynamicClientRegistrationAsync(
712715
private static string? GetResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
713716
=> protectedResourceMetadata.Resource;
714717

718+
private static bool IsLocalhostRedirectUri(Uri redirectUri)
719+
=> redirectUri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
720+
|| redirectUri.Host.Equals("127.0.0.1", StringComparison.Ordinal)
721+
|| redirectUri.Host.Equals("[::1]", StringComparison.Ordinal);
722+
715723
private string? GetScopeParameter(ProtectedResourceMetadata protectedResourceMetadata)
716724
{
717725
if (!string.IsNullOrEmpty(protectedResourceMetadata.WwwAuthenticateScope))

src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,20 @@ public sealed class DynamicClientRegistrationOptions
4646
/// </para>
4747
/// </remarks>
4848
public Func<DynamicClientRegistrationResponse, CancellationToken, Task>? ResponseDelegate { get; set; }
49+
50+
/// <summary>
51+
/// Gets or sets the application type to use during dynamic client registration.
52+
/// </summary>
53+
/// <remarks>
54+
/// <para>
55+
/// Valid values are "native" and "web". If not specified, the application type will be
56+
/// automatically determined based on the redirect URI: "native" for localhost/127.0.0.1
57+
/// redirect URIs, "web" for all others.
58+
/// </para>
59+
/// <para>
60+
/// Per the MCP specification, native applications (desktop, mobile, CLI, localhost web apps)
61+
/// should use "native", and web applications (remote browser-based) should use "web".
62+
/// </para>
63+
/// </remarks>
64+
public string? ApplicationType { get; set; }
4965
}

src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationRequest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,15 @@ internal sealed class DynamicClientRegistrationRequest
4848
/// </summary>
4949
[JsonPropertyName("scope")]
5050
public string? Scope { get; init; }
51+
52+
/// <summary>
53+
/// Gets or sets the application type for the client, as defined in OpenID Connect Dynamic Client Registration 1.0.
54+
/// </summary>
55+
/// <remarks>
56+
/// Valid values are "native" and "web". MCP clients MUST specify this during Dynamic Client Registration.
57+
/// Native applications (desktop, mobile, CLI, localhost web apps) should use "native".
58+
/// Web applications (remote browser-based) should use "web".
59+
/// </remarks>
60+
[JsonPropertyName("application_type")]
61+
public string? ApplicationType { get; init; }
5162
}

tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,4 +1261,82 @@ public async Task CanAuthenticate_WithLegacyServerUsingDefaultEndpointFallback()
12611261
await using var client = await McpClient.CreateAsync(
12621262
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
12631263
}
1264+
1265+
[Fact]
1266+
public async Task DynamicClientRegistration_SendsNativeApplicationType_ForLocalhostRedirectUri()
1267+
{
1268+
await using var app = await StartMcpServerAsync();
1269+
1270+
await using var transport = new HttpClientTransport(new()
1271+
{
1272+
Endpoint = new(McpServerUrl),
1273+
OAuth = new ClientOAuthOptions()
1274+
{
1275+
RedirectUri = new Uri("http://localhost:1179/callback"),
1276+
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
1277+
DynamicClientRegistration = new()
1278+
{
1279+
ClientName = "Test MCP Client",
1280+
},
1281+
},
1282+
}, HttpClient, LoggerFactory);
1283+
1284+
await using var client = await McpClient.CreateAsync(
1285+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
1286+
1287+
Assert.Equal("native", TestOAuthServer.LastRegistrationRequest?.ApplicationType);
1288+
}
1289+
1290+
[Fact]
1291+
public async Task DynamicClientRegistration_SendsWebApplicationType_ForNonLocalhostRedirectUri()
1292+
{
1293+
await using var app = await StartMcpServerAsync();
1294+
1295+
await using var transport = new HttpClientTransport(new()
1296+
{
1297+
Endpoint = new(McpServerUrl),
1298+
OAuth = new ClientOAuthOptions()
1299+
{
1300+
RedirectUri = new Uri("https://myapp.example.com/callback"),
1301+
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
1302+
DynamicClientRegistration = new()
1303+
{
1304+
ClientName = "Test MCP Client",
1305+
},
1306+
},
1307+
}, HttpClient, LoggerFactory);
1308+
1309+
await using var client = await McpClient.CreateAsync(
1310+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
1311+
1312+
Assert.Equal("web", TestOAuthServer.LastRegistrationRequest?.ApplicationType);
1313+
}
1314+
1315+
[Fact]
1316+
public async Task DynamicClientRegistration_UsesExplicitApplicationType_WhenConfigured()
1317+
{
1318+
await using var app = await StartMcpServerAsync();
1319+
1320+
await using var transport = new HttpClientTransport(new()
1321+
{
1322+
Endpoint = new(McpServerUrl),
1323+
OAuth = new ClientOAuthOptions()
1324+
{
1325+
// localhost redirect URI would normally auto-detect as "native",
1326+
// but the explicit setting should override it.
1327+
RedirectUri = new Uri("http://localhost:1179/callback"),
1328+
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
1329+
DynamicClientRegistration = new()
1330+
{
1331+
ClientName = "Test MCP Client",
1332+
ApplicationType = "web",
1333+
},
1334+
},
1335+
}, HttpClient, LoggerFactory);
1336+
1337+
await using var client = await McpClient.CreateAsync(
1338+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
1339+
1340+
Assert.Equal("web", TestOAuthServer.LastRegistrationRequest?.ApplicationType);
1341+
}
12641342
}

tests/ModelContextProtocol.TestOAuthServer/ClientRegistrationRequest.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace ModelContextProtocol.TestOAuthServer;
55
/// <summary>
66
/// Represents a client registration request as defined in RFC 7591.
77
/// </summary>
8-
internal sealed class ClientRegistrationRequest
8+
public sealed class ClientRegistrationRequest
99
{
1010
/// <summary>
1111
/// Gets or sets the redirect URIs for the client.
@@ -55,6 +55,12 @@ internal sealed class ClientRegistrationRequest
5555
[JsonPropertyName("scope")]
5656
public string? Scope { get; init; }
5757

58+
/// <summary>
59+
/// Gets or sets the application type.
60+
/// </summary>
61+
[JsonPropertyName("application_type")]
62+
public string? ApplicationType { get; init; }
63+
5864
/// <summary>
5965
/// Gets or sets the contacts for the client.
6066
/// </summary>

tests/ModelContextProtocol.TestOAuthServer/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
8181
public HashSet<string> DisabledMetadataPaths { get; } = new(StringComparer.OrdinalIgnoreCase);
8282
public IReadOnlyCollection<string> MetadataRequests => _metadataRequests.ToArray();
8383

84+
/// <summary>
85+
/// Gets the most recent dynamic client registration request received by the server.
86+
/// </summary>
87+
public ClientRegistrationRequest? LastRegistrationRequest { get; private set; }
88+
8489
/// <summary>
8590
/// Entry point for the application.
8691
/// </summary>
@@ -501,6 +506,8 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
501506
});
502507
}
503508

509+
LastRegistrationRequest = registrationRequest;
510+
504511
// Validate redirect URIs are provided
505512
if (registrationRequest.RedirectUris.Count == 0)
506513
{

0 commit comments

Comments
 (0)