Skip to content
Draft
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
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; }
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 as this is part of a not-yet-published version of the spec, it should be [Experimental]. Same for any other public surface area added in the PR.

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 fcf950e. DynamicClientRegistrationOptions.ApplicationType is now marked [Experimental(Experimentals.DcrApplicationType_DiagnosticId, UrlFormat = Experimentals.DcrApplicationType_Url)] (using MCPEXP001 since it's a spec feature). Added the corresponding constants to Experimentals.cs, updated docs/list-of-diagnostics.md, and added #pragma warning disable MCPEXP001 suppressions in ClientOAuthProvider.cs and the test that sets the property.

DynamicClientRegistrationRequest.ApplicationType is internal so it doesn't need the attribute.

}
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; }
}
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