Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/Common/Experimentals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,23 @@ internal static class Experimentals
/// URL for the experimental <c>RunSessionHandler</c> API.
/// </summary>
public const string RunSessionHandler_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp002";

/// <summary>
/// Diagnostic ID for the experimental <c>application_type</c> parameter in Dynamic Client Registration per SEP-837.
/// </summary>
/// <remarks>
/// This uses the same diagnostic ID as <see cref="Tasks_DiagnosticId"/> because it is an experimental
/// feature in the MCP specification itself.
/// </remarks>
public const string DcrApplicationType_DiagnosticId = "MCPEXP001";

/// <summary>
/// Message for the experimental <c>application_type</c> parameter in Dynamic Client Registration.
/// </summary>
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.";

/// <summary>
/// URL for the experimental <c>application_type</c> parameter in Dynamic Client Registration.
/// </summary>
public const string DcrApplicationType_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicClientRegistrationResponse, CancellationToken, Task>? _dcrResponseDelegate;
private readonly string? _dcrApplicationType;

private readonly HttpClient _httpClient;
private readonly ILogger _logger;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;

namespace ModelContextProtocol.Authentication;

/// <summary>
Expand Down Expand Up @@ -46,4 +48,21 @@ public sealed class DynamicClientRegistrationOptions
/// </para>
/// </remarks>
public Func<DynamicClientRegistrationResponse, CancellationToken, Task>? ResponseDelegate { get; set; }

/// <summary>
/// Gets or sets the application type to use during dynamic client registration.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// Per the MCP specification, native applications (desktop, mobile, CLI, localhost web apps)
/// should use "native", and web applications (remote browser-based) should use "web".
/// </para>
/// </remarks>
[Experimental(Experimentals.DcrApplicationType_DiagnosticId, UrlFormat = Experimentals.DcrApplicationType_Url)]
public string? ApplicationType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,17 @@ internal sealed class DynamicClientRegistrationRequest
/// </summary>
[JsonPropertyName("scope")]
public string? Scope { get; init; }

/// <summary>
/// Gets or sets the application type for the client, as defined in OpenID Connect Dynamic Client Registration 1.0.
/// </summary>
/// <remarks>
/// 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 <see cref="DynamicClientRegistrationOptions.ApplicationType"/>.
/// Native applications (desktop, mobile, CLI, localhost web apps) should use "native".
/// Web applications (remote browser-based) should use "web".
/// </remarks>
[JsonPropertyName("application_type")]
public string? ApplicationType { get; init; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this should also be annotated as experimental

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bff2ac4. DynamicClientRegistrationRequest.ApplicationType is now marked [Experimental(Experimentals.DcrApplicationType_DiagnosticId, UrlFormat = Experimentals.DcrApplicationType_Url)], with a corresponding #pragma warning disable MCPEXP001 suppression at the call site in ClientOAuthProvider.cs.

}
80 changes: 80 additions & 0 deletions tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ internal sealed class ClientRegistrationRequest
[JsonPropertyName("scope")]
public string? Scope { get; init; }

/// <summary>
/// Gets or sets the application type.
/// </summary>
[JsonPropertyName("application_type")]
public string? ApplicationType { get; init; }

/// <summary>
/// Gets or sets the contacts for the client.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions tests/ModelContextProtocol.TestOAuthServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
public HashSet<string> DisabledMetadataPaths { get; } = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyCollection<string> MetadataRequests => _metadataRequests.ToArray();

/// <summary>
/// Gets the application type from the most recent dynamic client registration request received by the server.
/// </summary>
public string? LastRegistrationApplicationType { get; private set; }

/// <summary>
/// Entry point for the application.
/// </summary>
Expand Down Expand Up @@ -501,6 +506,8 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
});
}

LastRegistrationApplicationType = registrationRequest.ApplicationType;

// Validate redirect URIs are provided
if (registrationRequest.RedirectUris.Count == 0)
{
Expand Down
Loading