Skip to content
Merged
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
36 changes: 30 additions & 6 deletions src/Netclaw.Daemon/Mcp/McpOAuthCredentialStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,21 @@ public McpOAuthTokenCache CreateTokenCache(
{
identity = new McpOAuthClientIdentity(configuredClientId, null, false);
}
else if (active is { ClientId: not null })
{
// Rebuild the provider identity from the persisted record whenever a client
// id exists, regardless of the DCR flag. Records written when the SDK ran its
// own dynamic registration persist the SDK-resolved client id without the DCR
// marker; discarding it on restart made SDK 2.0 skip the refresh path
// entirely ("null authorization result" on every later expiry).
identity = new McpOAuthClientIdentity(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM and nice catch lol

active.ClientId,
active.ClientSecret?.Value,
active.DynamicClientRegistration);
}
else
{
identity = active is { DynamicClientRegistration: true, ClientId: not null }
? new McpOAuthClientIdentity(active.ClientId, active.ClientSecret?.Value, true)
: new McpOAuthClientIdentity(null, null, false);
identity = new McpOAuthClientIdentity(null, null, false);
}

return new McpOAuthTokenCache(
Expand Down Expand Up @@ -361,6 +371,20 @@ private McpOAuthTokenSet CreateReplacement(
string canonicalResource)
{
var obtainedAt = tokens.ObtainedAt == default ? _timeProvider.GetUtcNow() : tokens.ObtainedAt;

// Netclaw's own registered identity wins when present. But when it is absent —
// e.g. the SDK performed its own dynamic client registration because Netclaw's
// registrar returned null (server does not advertise RFC 7591 support) or a
// client-metadata document supplied the id — the SDK's TokenContainer carries
// the client id/secret precisely so a durable cache survives a restart. Persisting
// a null client identity here made every cold-start refresh fall through to a
// new interactive authorization (the "null authorization result" loop observed on
// the Atlassian MCP nightly runs).
var clientId = identity.ClientId ?? tokens.ClientId;
var clientSecret = identity.ClientSecret ?? tokens.ClientSecret;
var dynamicRegistration = identity.DynamicClientRegistration
|| identity.ClientId is null && !string.IsNullOrWhiteSpace(tokens.ClientId);

var replacement = new McpOAuthTokenSet
{
AccessToken = new SensitiveString(tokens.AccessToken),
Expand All @@ -370,9 +394,9 @@ private McpOAuthTokenSet CreateReplacement(
TokenType = string.IsNullOrWhiteSpace(tokens.TokenType) ? "Bearer" : tokens.TokenType,
Scope = tokens.Scope,
ObtainedAt = obtainedAt,
ClientId = identity.ClientId,
ClientSecret = identity.ClientSecret is null ? null : new SensitiveString(identity.ClientSecret),
DynamicClientRegistration = identity.DynamicClientRegistration,
ClientId = clientId,
ClientSecret = clientSecret is null ? null : new SensitiveString(clientSecret),
DynamicClientRegistration = dynamicRegistration,
ResourceIdentity = canonicalResource,

// Prefer what the SDK just reported; fall back to the identity we registered
Expand Down
Loading