diff --git a/src/Netclaw.Daemon.Tests/Mcp/McpSdkOAuthFlowIntegrationTests.cs b/src/Netclaw.Daemon.Tests/Mcp/McpSdkOAuthFlowIntegrationTests.cs index e779007bb..370374e2f 100644 --- a/src/Netclaw.Daemon.Tests/Mcp/McpSdkOAuthFlowIntegrationTests.cs +++ b/src/Netclaw.Daemon.Tests/Mcp/McpSdkOAuthFlowIntegrationTests.cs @@ -66,6 +66,11 @@ public async Task ManagerExplicitAuthorization_PublishesOnlyAfterSdkExchangeAndT Assert.Equal("client-1", active!.ClientId); Assert.Equal("secret-client-1", active.ClientSecret?.Value); Assert.Equal("http://127.0.0.1:7331/api/mcp/oauth/callback", server.DynamicClientRegistrations.Single().RedirectUris.Single()); + Assert.Equal("netclaw", server.DynamicClientRegistrations.Single().ClientName); + Assert.Equal("https://netclaw.dev", server.DynamicClientRegistrations.Single().ClientUri); + Assert.Equal( + "https://raw.githubusercontent.com/netclaw-dev/netclaw-brand/dev/logo/netclaw-icon-purple.png", + server.DynamicClientRegistrations.Single().LogoUri); await harness.Runtime.LastHttpOptions!.OAuth!.TokenCache!.StoreTokensAsync( new TokenContainer @@ -1195,6 +1200,9 @@ public async Task HandleDynamicClientRegistrationAsync(HttpContext cont using var document = await JsonDocument.ParseAsync(context.Request.Body, cancellationToken: context.RequestAborted); var root = document.RootElement; + var clientName = ReadOptionalString(root, "client_name"); + var clientUri = ReadOptionalString(root, "client_uri"); + var logoUri = ReadOptionalString(root, "logo_uri"); var redirectUris = ReadStringArray(root, "redirect_uris"); var grantTypes = ReadStringArray(root, "grant_types"); var responseTypes = ReadStringArray(root, "response_types"); @@ -1207,6 +1215,9 @@ public async Task HandleDynamicClientRegistrationAsync(HttpContext cont _registrations.Enqueue(new DynamicClientRegistrationObservation( clientId, clientSecret, + clientName, + clientUri, + logoUri, redirectUris, grantTypes, responseTypes, @@ -1475,6 +1486,9 @@ private sealed record BrowserAuthorizationResult(string Code, string? State); private sealed record DynamicClientRegistrationObservation( string ClientId, string ClientSecret, + string? ClientName, + string? ClientUri, + string? LogoUri, IReadOnlyList RedirectUris, IReadOnlyList GrantTypes, IReadOnlyList ResponseTypes, diff --git a/src/Netclaw.Daemon/Mcp/McpOAuthClientRegistrar.cs b/src/Netclaw.Daemon/Mcp/McpOAuthClientRegistrar.cs index 73297ea79..20acee890 100644 --- a/src/Netclaw.Daemon/Mcp/McpOAuthClientRegistrar.cs +++ b/src/Netclaw.Daemon/Mcp/McpOAuthClientRegistrar.cs @@ -41,6 +41,20 @@ internal sealed class McpOAuthClientRegistrar( /// private const string SdkDefaultAuthMethod = "client_secret_post"; + /// + /// Home page presented to operators on the authorization server's consent screen. + /// RFC 7591 §2 client_uri. + /// + private const string ClientUri = "https://netclaw.dev"; + + /// + /// Netclaw logo presented on the authorization server's consent screen. + /// RFC 7591 §2 logo_uri. Served as a raw asset from the public brand + /// repo (square PNG icon) so any authorization server can fetch it without + /// a Netclaw deployment running, and PNG keeps SVG-picky servers happy. + /// + private const string LogoUri = "https://raw.githubusercontent.com/netclaw-dev/netclaw-brand/dev/logo/netclaw-icon-purple.png"; + /// /// Registers a client for and returns its identity. /// Returns null when the server advertises no OAuth protected-resource @@ -76,6 +90,8 @@ internal sealed class McpOAuthClientRegistrar( var request = new Dictionary { ["client_name"] = "netclaw", + ["client_uri"] = ClientUri, + ["logo_uri"] = LogoUri, ["redirect_uris"] = new[] { redirectUri.ToString() }, ["grant_types"] = new[] { "authorization_code", "refresh_token" }, ["response_types"] = new[] { "code" },