Skip to content
Merged
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
14 changes: 14 additions & 0 deletions src/Netclaw.Daemon.Tests/Mcp/McpSdkOAuthFlowIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1195,6 +1200,9 @@ public async Task<IResult> 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");
Expand All @@ -1207,6 +1215,9 @@ public async Task<IResult> HandleDynamicClientRegistrationAsync(HttpContext cont
_registrations.Enqueue(new DynamicClientRegistrationObservation(
clientId,
clientSecret,
clientName,
clientUri,
logoUri,
redirectUris,
grantTypes,
responseTypes,
Expand Down Expand Up @@ -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<string> RedirectUris,
IReadOnlyList<string> GrantTypes,
IReadOnlyList<string> ResponseTypes,
Expand Down
16 changes: 16 additions & 0 deletions src/Netclaw.Daemon/Mcp/McpOAuthClientRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ internal sealed class McpOAuthClientRegistrar(
/// </summary>
private const string SdkDefaultAuthMethod = "client_secret_post";

/// <summary>
/// Home page presented to operators on the authorization server's consent screen.
/// RFC 7591 §2 <c>client_uri</c>.
/// </summary>
private const string ClientUri = "https://netclaw.dev";

/// <summary>
/// Netclaw logo presented on the authorization server's consent screen.
/// RFC 7591 §2 <c>logo_uri</c>. 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.
/// </summary>
private const string LogoUri = "https://raw.githubusercontent.com/netclaw-dev/netclaw-brand/dev/logo/netclaw-icon-purple.png";

/// <summary>
/// Registers a client for <paramref name="endpoint"/> and returns its identity.
/// Returns <c>null</c> when the server advertises no OAuth protected-resource
Expand Down Expand Up @@ -76,6 +90,8 @@ internal sealed class McpOAuthClientRegistrar(
var request = new Dictionary<string, object>
{
["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" },
Expand Down
Loading